diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..62814d098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.gradle/ +.idea/ +build/ +*.iml +*.ipr +*.iws +*.class +bin/ +gen/ +out/ +/src/test/resources/config.properties \ No newline at end of file diff --git a/README.md b/README.md index be4b7cc9d..1e051bcc9 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# kotlin \ No newline at end of file + +# kotlin + +## Using your PubNub keys + +If you would like to run integration tests against your keys, Execute the following commands to add your publish, subscribe and secret keys to your local copy of the SDK: + + +```bash +cd src/test/resources/ +echo pub_key=YOUR_PUB_KEY >> config.properties +echo sub_key=YOUR_SUB_KEY >> config.properties +echo pam_pub_key=YOUR_PAM_PUB_KEY >> config.properties +echo pam_sub_key=YOUR_PAM_SUB_KEY >> config.properties +echo pam_sec_key=YOUR_PAM_SEC_KEY >> config.properties +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..3f5318a3a --- /dev/null +++ b/build.gradle @@ -0,0 +1,101 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id 'maven' + id 'com.github.johnrengelman.shadow' version '4.0.2' + id 'java-library' + id 'com.bmuschko.nexus' version '2.3.1' +} + +group = 'com.pubnub' +version = '4.0.0' + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + implementation("com.squareup.retrofit2:retrofit:2.6.2") + + api group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.12.6' + + api 'com.google.code.gson:gson:2.8.6' + implementation group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.6.2' + + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.28' + testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.28' + + testImplementation 'org.awaitility:awaitility-kotlin:4.0.1' + testImplementation 'com.github.tomakehurst:wiremock:2.26.3' + + implementation group: 'org.json', name: 'json', version: '20190722' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2' +} + +shadowJar { + classifier = "all" +} + +tasks.withType(Test) { + useJUnitPlatform { + includeEngines 'junit-jupiter' + } +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +extraArchive { + sources = true + tests = true + javadoc = true +} + +nexus { + sign = true + repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' + snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots' +} + +modifyPom { + project { + name 'PubNub Kotlin SDK' + description 'PubNub is a cross-platform client-to-client (1:1 and 1:many) push service in the cloud, capable of\n' + + ' broadcasting real-time messages to millions of web and mobile clients simultaneously, in less than a quarter\n' + + ' second!' + url 'https://github.com/pubnub/kotlin' + inceptionYear '2009' + + scm { + url 'https://github.com/pubnub/kotlin' + } + + licenses { + license { + name 'MIT License' + url 'https://github.com/pubnub/pubnub-api/blob/master/LICENSE' + distribution 'repo' + } + } + + developers { + developer { + id 'PubNub' + name 'PubNub' + email 'support@pubnub.com' + } + } + } +} + +build.finalizedBy(shadowJar) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..29e08e8ca --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..87b738cbd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..950461335 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 23 15:55:29 CEST 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..af6708ff2 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..6d57edc70 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..29161764f --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pubnub-kotlin' \ No newline at end of file diff --git a/src/main/java/com/pubnub/api/vendor/Base64.java b/src/main/java/com/pubnub/api/vendor/Base64.java new file mode 100644 index 000000000..901562636 --- /dev/null +++ b/src/main/java/com/pubnub/api/vendor/Base64.java @@ -0,0 +1,746 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pubnub.api.vendor; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs 2045 and 3548. + */ +public class Base64 { + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + + /** + * Flag to pass to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + + /* package */ static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(Charset.forName("UTF-8")), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int DECODE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int DECODE_WEBSAFE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Non-data values in the DECODE arrays. + */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + final private int[] alphabet; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3 / 4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p + 4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p + 1] & 0xff] << 12) | + (alphabet[input[p + 2] & 0xff] << 6) | + (alphabet[input[p + 3] & 0xff]))) >= 0) { + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op + 2] = (byte) value; + output[op + 1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op + 1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + try { + return new String(encode(input, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + try { + return new String(encode(input, offset, len, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: + break; + case 1: + output_len += 2; + break; + case 2: + output_len += 3; + break; + default: + break; + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + + assert encoder.op == output_len; + + return encoder.output; + } + + /* package */ static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * RFC 2045). + */ + public static final int LINE_GROUPS = 19; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE_WEBSAFE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + + final private byte[] tail; + /* package */ int tailLen; + private int count; + + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] alphabet; + + public Encoder(int flags, byte[] output) { + this.output = output; + + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8 / 5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + + int p = offset; + len += offset; + int v = -1; + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + switch (tailLen) { + case 0: + // There was no tail. + break; + case 1: + if (p + 2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + case 2: + if (p + 1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p + 3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p + 1] & 0xff) << 8) | + (input[p + 2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op + 1] = alphabet[(v >> 12) & 0x3f]; + output[op + 2] = alphabet[(v >> 6) & 0x3f]; + output[op + 3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p - tailLen == len - 1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p - tailLen == len - 2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len - 1) { + tail[tailLen++] = input[p]; + } else if (p == len - 2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p + 1]; + } + } + + this.op = op; + this.count = count; + + return true; + } + } + + private Base64() { + } // don't instantiate +} diff --git a/src/main/java/com/pubnub/api/vendor/Crypto.java b/src/main/java/com/pubnub/api/vendor/Crypto.java new file mode 100644 index 000000000..c63f95f40 --- /dev/null +++ b/src/main/java/com/pubnub/api/vendor/Crypto.java @@ -0,0 +1,149 @@ +package com.pubnub.api.vendor; + +import com.google.gson.Gson; +import com.pubnub.api.PubNubError; +import com.pubnub.api.PubNubException; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.spec.AlgorithmParameterSpec; + + +public class Crypto { + + byte[] keyBytes = null; + byte[] ivBytes = null; + String initializationVector = "0123456789012345"; + String cipherKey; + boolean INIT = false; + + public Crypto(String cipherKey) { + this.cipherKey = cipherKey; + } + + public Crypto(String cipherKey, String customInitializationVector) { + if (customInitializationVector != null) { + this.initializationVector = customInitializationVector; + } + + this.cipherKey = cipherKey; + } + + public void initCiphers() throws PubNubException { + if (INIT) + return; + try { + + keyBytes = new String(hexEncode(sha256(this.cipherKey.getBytes("UTF-8"))), "UTF-8") + .substring(0, 32) + .toLowerCase().getBytes("UTF-8"); + ivBytes = initializationVector.getBytes("UTF-8"); + INIT = true; + } catch (UnsupportedEncodingException e) { + throw newCryptoError(11, e); + } + } + + public static byte[] hexEncode(byte[] input) throws PubNubException { + StringBuffer result = new StringBuffer(); + for (byte byt : input) + result.append(Integer.toString((byt & 0xff) + 0x100, 16).substring(1)); + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw newCryptoError(12, e); + } + } + + public static PubNubException newCryptoError(int code, Exception exception) { + PubNubException pubNubException = new PubNubException(); + pubNubException.setPubnubError(PubNubError.CRYPTO_ERROR); + pubNubException.setErrorMessage(exception.getClass().getSimpleName() + " " + exception.getMessage() + " " + code); + return pubNubException; + } + + public String encrypt(String input) throws PubNubException { + try { + initCiphers(); + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = null; + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec); + return new String(Base64.encode(cipher.doFinal(input.getBytes("UTF-8")), 0), Charset.forName("UTF-8")); + } catch (Exception e) { + throw newCryptoError(0, e); + } + + } + + /** + * Decrypt + * + * @param cipher_text + * @return String + * @throws PubNubException + */ + public String decrypt(String cipher_text) throws PubNubException { + try { + initCiphers(); + AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes); + SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec); + return new String(cipher.doFinal(Base64.decode(cipher_text, 0)), "UTF-8"); + } catch (Exception e) { + throw newCryptoError(0, e); + } + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + /** + * Get MD5 + * + * @param input + * @return byte[] + * @throws PubNubException + */ + public static byte[] md5(String input) throws PubNubException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("MD5"); + byte[] hashedBytes = digest.digest(input.getBytes("UTF-8")); + return hashedBytes; + } catch (Exception e) { + throw newCryptoError(0, e); + } + } + + /** + * Get SHA256 + * + * @param input + * @return byte[] + * @throws PubNubException + */ + public static byte[] sha256(byte[] input) throws PubNubException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + byte[] hashedBytes = digest.digest(input); + return hashedBytes; + } catch (Exception e) { + throw newCryptoError(0, e); + } + } + +} diff --git a/src/main/kotlin/com/pubnub/api/Endpoint.kt b/src/main/kotlin/com/pubnub/api/Endpoint.kt new file mode 100644 index 000000000..4442a5a5b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/Endpoint.kt @@ -0,0 +1,425 @@ +package com.pubnub.api + +import com.google.gson.JsonElement +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.enums.PNStatusCategory.* +import com.pubnub.api.models.consumer.PNStatus +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.util.* + +abstract class Endpoint(protected val pubnub: PubNub) { + + companion object { + private const val SERVER_RESPONSE_SUCCESS = 200 + private const val SERVER_RESPONSE_BAD_REQUEST = 400 + private const val SERVER_RESPONSE_FORBIDDEN = 403 + private const val SERVER_RESPONSE_NOT_FOUND = 404 + } + + private lateinit var cachedCallback: (result: Output?, status: PNStatus) -> Unit + private lateinit var call: Call + private var silenceFailures = false + + var queryParam: Map = emptyMap() + + fun sync(): Output? { + validateParams() + + call = doWork(createBaseParams()) + + val response = + try { + call.execute() + } catch (e: Exception) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + } + } + + when { + response.isSuccessful -> { + storeRequestLatency(response) + return checkAndCreateResponse(response) + } + else -> { + val (errorString, errorJson) = extractErrorBody(response) + throw PubNubException(PubNubError.HTTP_ERROR).apply { + errorMessage = errorString + jso = errorJson.toString() + statusCode = response.code() + affectedCall = call + } + } + } + } + + fun async(callback: (result: Output?, status: PNStatus) -> Unit) { + cachedCallback = callback + + try { + validateParams() + call = doWork(createBaseParams()) + } catch (pubnubException: PubNubException) { + callback.invoke( + null, + createStatusResponse( + category = PNBadRequestCategory, + exception = pubnubException + ) + ) + return + } + + call.enqueue(object : Callback { + + override fun onResponse(call: Call, response: Response) { + + when { + response.isSuccessful -> { + // query params + storeRequestLatency(response) + try { + Triple(PNAcknowledgmentCategory, checkAndCreateResponse(response), null) + } catch (e: PubNubException) { + Triple(PNMalformedResponseCategory, null, e) + }.let { + callback.invoke( + it.second, + createStatusResponse( + category = it.first, + response = response, + exception = it.third + ) + ) + } + } + else -> { + val (errorString, errorJson) = extractErrorBody(response) + + val exception = PubNubException(PubNubError.HTTP_ERROR).apply { + errorMessage = errorString + jso = errorJson.toString() + statusCode = response.code() + affectedCall = call + } + + val pnStatusCategory = when (response.code()) { + SERVER_RESPONSE_FORBIDDEN -> PNAccessDeniedCategory + SERVER_RESPONSE_BAD_REQUEST -> PNBadRequestCategory + SERVER_RESPONSE_NOT_FOUND -> PNNotFoundCategory + else -> PNUnknownCategory + } + + callback.invoke( + null, + createStatusResponse( + category = pnStatusCategory, + response = response, + exception = exception, + errorBody = errorJson + ) + ) + return + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + + if (silenceFailures) { + return + } + + lateinit var pnStatusCategory: PNStatusCategory + + val pubnubException = PubNubException(t.toString()) + + try { + throw t + } catch (networkException: UnknownHostException) { + pubnubException.pubnubError = PubNubError.CONNECTION_NOT_SET + pnStatusCategory = PNUnexpectedDisconnectCategory + } catch (connectException: ConnectException) { + pubnubException.pubnubError = PubNubError.CONNECT_EXCEPTION + pnStatusCategory = PNUnexpectedDisconnectCategory + } catch (socketTimeoutException: SocketTimeoutException) { + pubnubException.pubnubError = PubNubError.SUBSCRIBE_TIMEOUT + pnStatusCategory = PNTimeoutCategory + } catch (ioException: IOException) { + pubnubException.pubnubError = PubNubError.PARSING_ERROR + pnStatusCategory = PNMalformedResponseCategory + } catch (ioException: IllegalStateException) { + pubnubException.pubnubError = PubNubError.PARSING_ERROR + pnStatusCategory = PNMalformedResponseCategory + } catch (throwable1: Throwable) { + pubnubException.pubnubError = PubNubError.HTTP_ERROR + pnStatusCategory = if (call.isCanceled) { + PNCancelledCategory + } else { + PNBadRequestCategory + } + } + + callback.invoke( + null, + createStatusResponse( + category = pnStatusCategory, + exception = pubnubException + ) + ) + } + }) + } + + private fun storeRequestLatency(response: Response) { + pubnub.telemetryManager.storeLatency( + latency = with(response.raw()) { + receivedResponseAtMillis() - sentRequestAtMillis() + }, + type = operationType() + ) + } + + private fun createBaseParams(): HashMap { + val map = hashMapOf() + + map.putAll(queryParam) + + map["pnsdk"] = "PubNub-Kotlin/${pubnub.version}" + map["uuid"] = pubnub.configuration.uuid + + if (pubnub.configuration.includeInstanceIdentifier) { + map["instanceid"] = pubnub.instanceId + } + + if (pubnub.configuration.includeRequestIdentifier) { + map["requestid"] = pubnub.requestId() + } + + if (isAuthRequired() && pubnub.configuration.isAuthKeyValid()) { + map["auth"] = pubnub.configuration.authKey + } + + map.putAll(pubnub.telemetryManager.operationsLatency()) + return map + } + + /** + * cancel the operation but do not alert anybody, useful for restarting the heartbeats and subscribe loops. + */ + fun silentCancel() { + if (::call.isInitialized) { + if (!call.isCanceled) { + silenceFailures = true + call.cancel() + } + } + } + + + private fun createStatusResponse( + category: PNStatusCategory, + response: Response? = null, + exception: PubNubException? = null, + errorBody: JsonElement? = null + ): PNStatus { + + val pnStatus = PNStatus( + category = category, + error = response == null || exception != null, + operation = operationType(), + exception = exception + ) + + pnStatus.executedEndpoint = this + + response?.let { + + with(pnStatus) { + statusCode = it.code() + tlsEnabled = it.raw().request().url().isHttps + origin = it.raw().request().url().host() + uuid = it.raw().request().url().queryParameter("uuid") + authKey = it.raw().request().url().queryParameter("auth") + clientRequest = it.raw().request() + } + } + + val errorChannels = mutableListOf() + val errorGroups = mutableListOf() + + if (errorBody != null) { + if (pubnub.mapper.isJsonObject(errorBody) && pubnub.mapper.hasField(errorBody, "payload")) { + + val payloadBody = pubnub.mapper.getField(errorBody, "payload")!! + + if (pubnub.mapper.hasField(payloadBody, "channels")) { + val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channels") + while (iterator.hasNext()) { + errorChannels.add(pubnub.mapper.elementToString(iterator.next())!!) + } + } + + if (pubnub.mapper.hasField(payloadBody, "channel-groups")) { + val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channel-groups") + while (iterator.hasNext()) { + val node = iterator.next() + + val channelGroupName = pubnub.mapper.elementToString(node)!!.let { + if (it.first().toString() == ":") { + it.substring(1) + } else { + it + } + } + + errorGroups.add(channelGroupName) + } + } + } + } + + pnStatus.affectedChannels = + if (errorChannels.isNotEmpty()) { + errorChannels + } else { + try { + getAffectedChannels() + } catch (e: UninitializedPropertyAccessException) { + emptyList() + } + } + + pnStatus.affectedChannelGroups = + if (errorGroups.isNotEmpty()) { + errorGroups + } else { + try { + getAffectedChannelGroups() + } catch (e: UninitializedPropertyAccessException) { + emptyList() + } + } + + return pnStatus + } + + internal fun retry() { + silenceFailures = false + async(cachedCallback) + } + + private fun extractErrorBody(response: Response): Pair { + val errorBodyString = try { + response.errorBody()?.string() + } catch (e: IOException) { + "N/A" + } + + val errorBodyJson = try { + pubnub.mapper.fromJson(errorBodyString, JsonElement::class.java) + } catch (e: PubNubException) { + null + } + + return errorBodyString to errorBodyJson + } + + private fun checkAndCreateResponse(input: Response): Output? { + try { + return createResponse(input) + } catch (pubnubException: PubNubException) { + throw pubnubException.apply { + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + affectedCall = call + } + } catch (e: KotlinNullPointerException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: IllegalStateException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: IndexOutOfBoundsException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: NullPointerException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: IllegalArgumentException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: TypeCastException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: ClassCastException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } catch (e: UninitializedPropertyAccessException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.toString() + affectedCall = call + statusCode = input.code() + jso = pubnub.mapper.toJson(input.body()) + } + } + } + + protected open fun getAffectedChannels() = emptyList() + protected open fun getAffectedChannelGroups(): List = emptyList() + + protected open fun validateParams() { + if (isSubKeyRequired() && !pubnub.configuration.isSubscribeKeyValid()) { + throw PubNubException(PubNubError.SUBSCRIBE_KEY_MISSING) + } + if (isPubKeyRequired() && !pubnub.configuration.isPublishKeyValid()) { + throw PubNubException(PubNubError.PUBLISH_KEY_MISSING) + } + } + + protected abstract fun doWork(queryParams: HashMap): Call + protected abstract fun createResponse(input: Response): Output? + + protected abstract fun operationType(): PNOperationType + + protected open fun isSubKeyRequired() = true + protected open fun isPubKeyRequired() = false + protected open fun isAuthRequired() = true +} + diff --git a/src/main/kotlin/com/pubnub/api/PNConfiguration.kt b/src/main/kotlin/com/pubnub/api/PNConfiguration.kt new file mode 100644 index 000000000..9c472cf93 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/PNConfiguration.kt @@ -0,0 +1,99 @@ +package com.pubnub.api + +import com.pubnub.api.enums.PNHeartbeatNotificationOptions +import com.pubnub.api.enums.PNLogVerbosity +import com.pubnub.api.enums.PNReconnectionPolicy +import okhttp3.Authenticator +import okhttp3.CertificatePinner +import okhttp3.ConnectionSpec +import okhttp3.logging.HttpLoggingInterceptor +import org.slf4j.LoggerFactory +import java.net.Proxy +import java.net.ProxySelector +import java.util.* +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509ExtendedTrustManager + +class PNConfiguration { + + private val log = LoggerFactory.getLogger("PNConfiguration") + + private companion object Constants { + private const val DEFAULT_DEDUPE_SIZE = 100 + private const val PRESENCE_TIMEOUT = 300 + private const val MINIMUM_PRESENCE_TIMEOUT = 20 + private const val NON_SUBSCRIBE_REQUEST_TIMEOUT = 10 + private const val SUBSCRIBE_TIMEOUT = 310 + private const val CONNECT_TIMEOUT = 5 + } + + lateinit var subscribeKey: String + lateinit var publishKey: String + lateinit var secretKey: String + lateinit var authKey: String + lateinit var cipherKey: String + + var uuid: String = "pn-${UUID.randomUUID()}" + + lateinit var origin: String + var secure = true + + var logVerbosity = PNLogVerbosity.NONE + var heartbeatNotificationOptions = PNHeartbeatNotificationOptions.FAILURES + var reconnectionPolicy = PNReconnectionPolicy.NONE + + var presenceTimeout = PRESENCE_TIMEOUT + set(value) { + field = + if (value < MINIMUM_PRESENCE_TIMEOUT) { + log.warn("Presence timeout is too low. Defaulting to: $MINIMUM_PRESENCE_TIMEOUT") + MINIMUM_PRESENCE_TIMEOUT + } else value + heartbeatInterval = (presenceTimeout / 2) - 1 + } + + var heartbeatInterval = 0 + + var subscribeTimeout = SUBSCRIBE_TIMEOUT + var connectTimeout = CONNECT_TIMEOUT + var nonSubscribeRequestTimeout = + NON_SUBSCRIBE_REQUEST_TIMEOUT + var maximumMessagesCacheSize = DEFAULT_DEDUPE_SIZE + + var cacheBusting = false + + var suppressLeaveEvents = false + var disableTokenManager = false + lateinit var filterExpression: String + var includeInstanceIdentifier = false + var includeRequestIdentifier = true + var maximumReconnectionRetries = -1 + var maximumConnections: Int? = null + var requestMessageCountThreshold: Int? = null + var googleAppEngineNetworking = false + var startSubscriberThread = true + var dedupOnSubscribe = false + + var proxy: Proxy? = null + var proxySelector: ProxySelector? = null + var proxyAuthenticator: Authenticator? = null + var certificatePinner: CertificatePinner? = null + var httpLoggingInterceptor: HttpLoggingInterceptor? = null + var sslSocketFactory: SSLSocketFactory? = null + var x509ExtendedTrustManager: X509ExtendedTrustManager? = null + var connectionSpec: ConnectionSpec? = null + var hostnameVerifier: HostnameVerifier? = null + + internal fun isSubscribeKeyValid() = ::subscribeKey.isInitialized && !subscribeKey.isBlank() + internal fun isAuthKeyValid() = ::authKey.isInitialized && !authKey.isBlank() + internal fun isCipherKeyValid() = ::cipherKey.isInitialized && !cipherKey.isBlank() + internal fun isPublishKeyValid() = ::publishKey.isInitialized && !publishKey.isBlank() + internal fun isSecretKeyValid() = ::secretKey.isInitialized && !secretKey.isBlank() + internal fun isOriginValid() = ::origin.isInitialized && !origin.isBlank() + internal fun isFilterExpressionKeyValid(function: String.() -> Unit) { + if (::filterExpression.isInitialized && !filterExpression.isBlank()) { + function.invoke(filterExpression) + } + } +} diff --git a/src/main/kotlin/com/pubnub/api/PubNub.kt b/src/main/kotlin/com/pubnub/api/PubNub.kt new file mode 100644 index 000000000..ebd25a80e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/PubNub.kt @@ -0,0 +1,168 @@ +package com.pubnub.api + +import com.pubnub.api.builder.PresenceBuilder +import com.pubnub.api.builder.SubscribeBuilder +import com.pubnub.api.builder.UnsubscribeBuilder +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.endpoints.* +import com.pubnub.api.endpoints.access.Grant +import com.pubnub.api.endpoints.channel_groups.* +import com.pubnub.api.endpoints.message_actions.AddMessageAction +import com.pubnub.api.endpoints.message_actions.GetMessageActions +import com.pubnub.api.endpoints.message_actions.RemoveMessageAction +import com.pubnub.api.endpoints.presence.GetState +import com.pubnub.api.endpoints.presence.HereNow +import com.pubnub.api.endpoints.presence.SetState +import com.pubnub.api.endpoints.presence.WhereNow +import com.pubnub.api.endpoints.pubsub.Publish +import com.pubnub.api.endpoints.pubsub.Signal +import com.pubnub.api.endpoints.push.AddChannelsToPush +import com.pubnub.api.endpoints.push.ListPushProvisions +import com.pubnub.api.endpoints.push.RemoveAllPushChannelsForDevice +import com.pubnub.api.endpoints.push.RemoveChannelsFromPush +import com.pubnub.api.managers.* +import com.pubnub.api.vendor.Crypto +import java.util.* + +class PubNub(val configuration: PNConfiguration) { + + private companion object Constants { + private const val TIMESTAMP_DIVIDER = 1000 + private const val SDK_VERSION = "4.0.0-dev" + private const val MAX_SEQUENCE = 65535; + } + + private val basePathManager = BasePathManager(configuration) + + val mapper = MapperManager() + internal val retrofitManager = RetrofitManager(this) + internal val publishSequenceManager = PublishSequenceManager(MAX_SEQUENCE) + internal val telemetryManager = TelemetryManager() + internal val subscriptionManager = SubscriptionManager(this) + + val version = SDK_VERSION + val instanceId = UUID.randomUUID().toString() + + fun baseUrl() = basePathManager.basePath() + fun requestId() = UUID.randomUUID().toString() + fun timestamp() = (Date().time / TIMESTAMP_DIVIDER).toInt() + + fun publish() = Publish(this) + fun fire() = Publish(this).apply { + shouldStore = false + replicate = false + } + + fun signal() = Signal(this) + fun subscribe() = SubscribeBuilder(subscriptionManager) + fun unsubscribe() = UnsubscribeBuilder(subscriptionManager) + fun presence() = PresenceBuilder(subscriptionManager) + + fun addPushNotificationsOnChannels() = AddChannelsToPush(this) + fun removePushNotificationsFromChannels() = + RemoveChannelsFromPush(this) + + fun removeAllPushNotificationsFromDeviceWithPushToken() = + RemoveAllPushChannelsForDevice(this) + + fun auditPushChannelProvisions() = ListPushProvisions(this) + + fun history() = History(this) + fun messageCounts() = MessageCounts(this) + fun fetchMessages() = FetchMessages(this) + fun deleteMessages() = DeleteMessages(this) + fun hereNow() = HereNow(this) + fun whereNow() = WhereNow(this) + fun setPresenceState() = SetState(this) + fun getPresenceState() = GetState(this) + fun time() = Time(this) + fun addMessageAction() = AddMessageAction(this) + fun getMessageActions() = GetMessageActions(this) + fun removeMessageAction() = RemoveMessageAction(this) + + fun listAllChannelGroups() = ListAllChannelGroup(this) + fun listChannelsForChannelGroup() = AllChannelsChannelGroup(this) + fun addChannelsToChannelGroup() = AddChannelChannelGroup(this) + fun removeChannelsFromChannelGroup() = RemoveChannelChannelGroup(this) + fun deleteChannelGroup() = DeleteChannelGroup(this) + + fun grant() = Grant(this) + + fun addListener(listener: SubscribeCallback) { + subscriptionManager.addListener(listener) + } + + fun removeListener(listener: SubscribeCallback) { + subscriptionManager.removeListener(listener) + } + + fun getSubscribedChannels() = subscriptionManager.getSubscribedChannels() + fun getSubscribedChannelGroups() = subscriptionManager.getSubscribedChannelGroups() + fun unsubscribeAll() = subscriptionManager.unsubscribeAll() + + /** + * Perform Cryptographic decryption of an input string using cipher key provided by PNConfiguration + * + * @param inputString String to be encrypted + * @return String containing the encryption of inputString using cipherKey + */ + fun decrypt(inputString: String): String { + return decrypt(inputString, configuration.cipherKey) + } + + /** + * Perform Cryptographic decryption of an input string using the cipher key + * + * @param inputString String to be encrypted + * @param cipherKey cipher key to be used for encryption + * @return String containing the encryption of inputString using cipherKey + * @throws PubNubException throws exception in case of failed encryption + */ + fun decrypt(inputString: String, cipherKey: String): String { + return Crypto(cipherKey).decrypt(inputString) + } + + /** + * Perform Cryptographic encryption of an input string and the cipher key provided by PNConfiguration + * + * @param inputString String to be encrypted + * @return String containing the encryption of inputString using cipherKey + */ + fun encrypt(inputString: String): String { + return encrypt(inputString, configuration.cipherKey) + } + + /** + * Perform Cryptographic encryption of an input string and the cipher key. + * + * @param inputString String to be encrypted + * @param cipherKey cipher key to be used for encryption + * @return String containing the encryption of inputString using cipherKey + * @throws PubNubException throws exception in case of failed encryption + */ + @Throws(PubNubException::class) + fun encrypt(inputString: String, cipherKey: String): String { + return Crypto(cipherKey).encrypt(inputString) + } + + fun reconnect() { + subscriptionManager.reconnect() + } + + fun disconnect() { + subscriptionManager.disconnect() + } + + fun destroy() { + subscriptionManager.destroy() + retrofitManager.destroy() + } + + fun forceDestroy() { + subscriptionManager.destroy(true) + retrofitManager.destroy(true) + telemetryManager.stopCleanUpTimer() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/PubNubError.kt b/src/main/kotlin/com/pubnub/api/PubNubError.kt new file mode 100644 index 000000000..766607c9e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/PubNubError.kt @@ -0,0 +1,159 @@ +package com.pubnub.api + +enum class PubNubError(val code: Int, val message: String) { + + TIMEOUT( + 100, + "Timeout Occurred" + ), + + CONNECT_EXCEPTION( + 102, + "Connect Exception. Please verify if network is reachable" + ), + + SECRET_KEY_MISSING( + 114, + "ULS configuration failed. Secret Key not configured" + ), + + JSON_ERROR( + 121, + "JSON Error while processing API response" + ), + + PARSING_ERROR( + 126, + "Parsing Error" + ), + + CONNECTION_NOT_SET( + 133, + "PubNub Connection not set" + ), + + GROUP_MISSING( + 136, + "Group Missing" + ), + + SUBSCRIBE_KEY_MISSING( + 138, + "ULS configuration failed. Subscribe Key not configured." + ), + + PUBLISH_KEY_MISSING( + 139, + "ULS configuration failed. Publish Key not configured." + ), + + SUBSCRIBE_TIMEOUT( + 130, + "Subscribe Timeout" + ), + + HTTP_ERROR( + 103, + "HTTP Error. Please check network connectivity. Please contact support with error details if the issue persists." + ), + + MESSAGE_MISSING( + 142, + "Message Missing" + ), + + CHANNEL_MISSING( + 132, + "Channel Missing" + ), + + CRYPTO_ERROR( + 135, + "Error while encrypting/decrypting message. Please contact support with error details." + ), + + STATE_MISSING( + 140, + "State Missing." + ), + + CHANNEL_AND_GROUP_MISSING( + 141, + "Channel and Group Missing." + ), + + PUSH_TYPE_MISSING( + 143, + "Push Type Missing." + ), + + DEVICE_ID_MISSING( + 144, + "Device ID Missing" + ), + + TIMETOKEN_MISSING( + 145, + "Timetoken Missing." + ), + + CHANNELS_TIMETOKEN_MISMATCH( + 146, + "Channels and timetokens are not equal in size." + ), + + USER_MISSING( + 147, + "User is missing" + ), + + USER_ID_MISSING( + 148, + "User ID is missing" + ), + + USER_NAME_MISSING( + 149, + "User name is missing" + ), + + MESSAGE_ACTION_MISSING( + 158, + "Message action is missing." + ), + + MESSAGE_ACTION_TYPE_MISSING( + 159, + "Message action type is missing." + ), + + MESSAGE_ACTION_VALUE_MISSING( + 160, + "Message action value is missing." + ), + + MESSAGE_TIMETOKEN_MISSING( + 161, + "Message timetoken is missing." + ), + + MESSAGE_ACTION_TIMETOKEN_MISSING( + 162, + "Message action timetoken is missing." + ), + + HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS( + 163, + "History can return message action data for a single channel only. Either pass a single channel or disable the includeMessageActions flag." + ), + + PUSH_TOPIC_MISSING( + 164, + "Push notification topic is missing. Required only if push type is APNS2." + ); + + override fun toString(): String { + return "PubNubError(name=$name, code=$code, message='$message')" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/PubNubException.kt b/src/main/kotlin/com/pubnub/api/PubNubException.kt new file mode 100644 index 000000000..1bf62cf09 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/PubNubException.kt @@ -0,0 +1,18 @@ +package com.pubnub.api + +import retrofit2.Call + + +data class PubNubException( + var errorMessage: String? = null, + var pubnubError: PubNubError? = null, + var jso: String? = null, + var statusCode: Int = 0, + var affectedCall: Call<*>? = null +) : Exception(errorMessage) { + + constructor(pubnubError: PubNubError) : this( + errorMessage = pubnubError.message, + pubnubError = pubnubError + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/PubNubUtil.kt b/src/main/kotlin/com/pubnub/api/PubNubUtil.kt new file mode 100644 index 000000000..b2ec58f84 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/PubNubUtil.kt @@ -0,0 +1,213 @@ +package com.pubnub.api + +import com.pubnub.api.vendor.Base64 +import com.pubnub.api.vendor.Crypto +import okhttp3.Request +import okio.Buffer +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.Charset +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +internal class PubNubUtil { + + companion object A { + + private val log = LoggerFactory.getLogger("PubNubUtil") + + private const val CHARSET = "UTF-8" + + fun replaceLast(string: String, toReplace: String, replacement: String): String { + val pos = string.lastIndexOf(toReplace) + return if (pos > -1) { + string.substring(0, pos) + replacement + string.substring( + pos + toReplace.length, + string.length + ) + } else { + string + } + } + + /** + * Returns decoded String + * + * @param stringToEncode , input string + * @return , decoded string + */ + fun urlDecode(stringToEncode: String?): String? { + return try { + URLDecoder.decode(stringToEncode, CHARSET) + } catch (e: UnsupportedEncodingException) { + null + } + } + + fun signRequest( + originalRequest: Request, + pnConfiguration: PNConfiguration, + timestamp: Int + ): Request { + // only sign if we have a secret key in place. + if (!pnConfiguration.isSecretKeyValid()) { + return originalRequest + } + val signature = generateSignature(pnConfiguration, originalRequest, timestamp) + val rebuiltUrl = originalRequest.url().newBuilder() + .addQueryParameter("timestamp", timestamp.toString()) + .addQueryParameter("signature", signature) + .build() + return originalRequest.newBuilder().url(rebuiltUrl).build() + } + + internal fun signSHA256(key: String, data: String): String { + val sha256HMAC: Mac + val hmacData: ByteArray + val secretKey = SecretKeySpec(key.toByteArray(charset(CHARSET)), "HmacSHA256") + sha256HMAC = try { + Mac.getInstance("HmacSHA256") + } catch (e: NoSuchAlgorithmException) { + throw Crypto.newCryptoError(0, e) + } + try { + sha256HMAC.init(secretKey) + } catch (e: InvalidKeyException) { + throw Crypto.newCryptoError(0, e) + } + hmacData = sha256HMAC.doFinal(data.toByteArray(charset(CHARSET))) + val signedd = String(Base64.encode(hmacData, 0), Charset.forName(CHARSET)) + .replace('+', '-') + .replace('/', '_') + .replace("\n", "") + return signedd + } + + private fun generateSignature( + configuration: PNConfiguration, + request: Request, + timestamp: Int + ): String? { + val isV2Signature: Boolean + val signatureBuilder = StringBuilder() + val requestURL = request.url().encodedPath() + val queryParams = mutableMapOf() + for (queryKey in request.url().queryParameterNames()) { + queryParams[queryKey] = request.url().queryParameter(queryKey)!! + // queryParams[queryKey] = request.url().encoded + } + queryParams["timestamp"] = timestamp.toString() + + // todo AB testing + val classic = true + val encodedQueryString = if (classic) { + preparePamArguments(queryParams) + } else { + preparePamArguments("${request.url().encodedQuery()}×tamp=${timestamp}") + } + + isV2Signature = !(requestURL.startsWith("/publish") && request.method().equals("post", ignoreCase = true)) + if (!isV2Signature) { + signatureBuilder.append(configuration.subscribeKey).append("\n") + signatureBuilder.append(configuration.publishKey).append("\n") + signatureBuilder.append(requestURL).append("\n") + signatureBuilder.append(encodedQueryString) + } else { + signatureBuilder.append(request.method().toUpperCase()).append("\n") + signatureBuilder.append(configuration.publishKey).append("\n") + signatureBuilder.append(requestURL).append("\n") + signatureBuilder.append(encodedQueryString).append("\n") + signatureBuilder.append(PubNubUtil.requestBodyToString(request)) + } + + var signature = "" + try { + signature = signSHA256(configuration.secretKey, signatureBuilder.toString()) + if (isV2Signature) { + signature = removeTrailingEqualSigns(signature) + signature = "v2.$signature" + } + } catch (e: PubNubException) { + log.warn("signature failed on SignatureInterceptor: $e") + } catch (e: UnsupportedEncodingException) { + log.warn("signature failed on SignatureInterceptor: $e") + } + return signature + } + + fun removeTrailingEqualSigns(signature: String): String { + var cleanSignature = signature + while (cleanSignature[cleanSignature.length - 1] == '=') { + cleanSignature = cleanSignature.substring(0, cleanSignature.length - 1) + } + return cleanSignature + } + + internal fun requestBodyToString(request: Request): String? { + if (request.body() == null) { + return "" + } + try { + val buffer = Buffer() + request.body()!!.writeTo(buffer) + return buffer.readUtf8() + } catch (e: IOException) { + e.printStackTrace() + } + return "" + } + + internal fun preparePamArguments(pamArgs: Map): String { + val pamKeys: Set = TreeSet(pamArgs.keys) + var stringifiedArguments = "" + var i = 0 + for (pamKey in pamKeys) { + if (i != 0) { + stringifiedArguments = "$stringifiedArguments&" + } + stringifiedArguments = stringifiedArguments + pamKey + "=" + pamEncode(pamArgs[pamKey]!!) + i += 1 + } + return stringifiedArguments + } + + private fun preparePamArguments(encodedQueryString: String): String { + return encodedQueryString.split("&") + .toSortedSet() + .map { pamEncode(it, true) } + .joinToString("&") + } + + /** + * Returns encoded String + * + * @param stringToEncode , input string + * @return , encoded string + */ + internal fun pamEncode(stringToEncode: String, alreadyPercentEncoded: Boolean = false): String { + /* !'()*~ */ + + return if (alreadyPercentEncoded) { + stringToEncode + } else { + URLEncoder.encode(stringToEncode, "UTF-8") + .replace("+", "%20") + }.run { + replace("*", "%2A") + } + } + + } +} + +internal fun List.toCsv(): String { + if (this.isNotEmpty()) + return this.joinToString(",") + return "," +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt b/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt new file mode 100644 index 000000000..5a3b5ab23 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/builder/PubSubBuilders.kt @@ -0,0 +1,58 @@ +package com.pubnub.api.builder + +import com.pubnub.api.managers.SubscriptionManager + +abstract class PubSubBuilder( + protected val subscriptionManager: SubscriptionManager, + var channels: List = emptyList(), + var channelGroups: List = emptyList() +) { + abstract fun execute() +} + +class PresenceBuilder( + subscriptionManager: SubscriptionManager, + channels: List = emptyList(), + channelGroups: List = emptyList(), + var connected: Boolean = false +) : PubSubBuilder(subscriptionManager, channels, channelGroups) { + + override fun execute() { + val presenceOperation = PresenceOperation().apply { + connected = this@PresenceBuilder.connected + channels = this@PresenceBuilder.channels + channelGroups = this@PresenceBuilder.channelGroups + } + subscriptionManager.adaptPresenceBuilder(presenceOperation) + } +} + +class SubscribeBuilder( + subscriptionManager: SubscriptionManager, + var withPresence: Boolean = false, + var withTimetoken: Long = 0L +) : PubSubBuilder(subscriptionManager) { + + override fun execute() { + val subscribeOperation = SubscribeOperation().apply { + channels = this@SubscribeBuilder.channels + channelGroups = this@SubscribeBuilder.channelGroups + presenceEnabled = this@SubscribeBuilder.withPresence + timetoken = this@SubscribeBuilder.withTimetoken + } + this.subscriptionManager.adaptSubscribeBuilder(subscribeOperation) + } +} + +class UnsubscribeBuilder( + subscriptionManager: SubscriptionManager +) : PubSubBuilder(subscriptionManager) { + + override fun execute() { + val unsubscribeOperation = UnsubscribeOperation().apply { + channels = this@UnsubscribeBuilder.channels + channelGroups = this@UnsubscribeBuilder.channelGroups + } + this.subscriptionManager.adaptUnsubscribeBuilder(unsubscribeOperation) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt b/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt new file mode 100644 index 000000000..23259dfbc --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/builder/PubSubOperations.kt @@ -0,0 +1,25 @@ +package com.pubnub.api.builder + +// basic publish/subscribe class + +internal abstract class PubSubOperation( + internal var channels: List = emptyList(), + internal var channelGroups: List = emptyList() +) + +// concrete publish/subscribe cases + +internal class SubscribeOperation( + internal var presenceEnabled: Boolean = false, + internal var timetoken: Long = 0L +) : PubSubOperation() + +internal class UnsubscribeOperation : PubSubOperation() + +internal class PresenceOperation( + internal var connected: Boolean = false +) : PubSubOperation() + +internal class StateOperation( + var state: Any? = null +) : PubSubOperation() \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt b/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt new file mode 100644 index 000000000..79f01bfda --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/callbacks/ReconnectionCallback.kt @@ -0,0 +1,6 @@ +package com.pubnub.api.callbacks + +internal abstract class ReconnectionCallback { + abstract fun onReconnection() + abstract fun onMaxReconnectionExhaustion() +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt b/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt new file mode 100644 index 000000000..0687d12b0 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/callbacks/SubscribeCallback.kt @@ -0,0 +1,20 @@ +package com.pubnub.api.callbacks + +import com.pubnub.api.PubNub +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult + +abstract class SubscribeCallback { + abstract fun status(pubnub: PubNub, pnStatus: PNStatus) + + open fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {} + + open fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {} + + open fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) {} + + open fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) {} +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt b/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt new file mode 100644 index 000000000..10cb19bdc --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/DeleteMessages.kt @@ -0,0 +1,43 @@ +package com.pubnub.api.endpoints + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNDeleteMessagesResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class DeleteMessages(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channels: List + var start: Long? = null + var end: Long? = null + + override fun validateParams() { + super.validateParams() + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call { + start?.let { + queryParams["start"] = it.toString().toLowerCase(Locale.US) + } + end?.let { + queryParams["end"] = it.toString().toLowerCase(Locale.US) + } + + return pubnub.retrofitManager.historyService.deleteMessages( + pubnub.configuration.subscribeKey, + channels.toCsv(), + queryParams + ) + } + + override fun createResponse(input: Response): PNDeleteMessagesResult? { + return PNDeleteMessagesResult() + } + + override fun operationType() = PNOperationType.PNDeleteMessagesOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt b/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt new file mode 100644 index 000000000..e9961e539 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/FetchMessages.kt @@ -0,0 +1,139 @@ +package com.pubnub.api.endpoints + +import com.google.gson.JsonElement +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNFetchMessageItem +import com.pubnub.api.models.consumer.history.PNFetchMessagesResult +import com.pubnub.api.models.server.FetchMessagesEnvelope +import com.pubnub.api.vendor.Crypto +import org.slf4j.LoggerFactory +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class FetchMessages(pubnub: PubNub) : Endpoint(pubnub) { + + private val log = LoggerFactory.getLogger("FetchMessages") + + private companion object { + private const val DEFAULT_MESSAGES = 1 + private const val MAX_MESSAGES = 25 + } + + lateinit var channels: List + var maximumPerChannel = 0 + var start: Long? = null + var end: Long? = null + var includeMeta = false + var includeMessageActions = false + + override fun validateParams() { + super.validateParams() + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + + if (!includeMessageActions) { + if (maximumPerChannel !in DEFAULT_MESSAGES..MAX_MESSAGES) { + when { + maximumPerChannel < DEFAULT_MESSAGES -> maximumPerChannel = DEFAULT_MESSAGES + maximumPerChannel > MAX_MESSAGES -> maximumPerChannel = MAX_MESSAGES + } + log.info("maximumPerChannel param defaulting to $maximumPerChannel") + } + } else { + if (maximumPerChannel !in DEFAULT_MESSAGES..MAX_MESSAGES) { + maximumPerChannel = MAX_MESSAGES + log.info("maximumPerChannel param defaulting to $maximumPerChannel") + } + } + } + + override fun getAffectedChannels() = channels + + override fun doWork(queryParams: HashMap): Call { + queryParams["max"] = maximumPerChannel.toString() + + start?.let { + queryParams["start"] = it.toString().toLowerCase(Locale.US) + } + end?.let { + queryParams["end"] = it.toString().toLowerCase(Locale.US) + } + + if (includeMeta) { + queryParams["include_meta"] = includeMeta.toString() + } + + return if (!includeMessageActions) { + pubnub.retrofitManager.historyService.fetchMessages( + subKey = pubnub.configuration.subscribeKey, + channels = channels.toCsv(), + options = queryParams + ) + } else { + if (channels.size > 1) { + throw PubNubException(PubNubError.HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS) + } + pubnub.retrofitManager.historyService.fetchMessagesWithActions( + subKey = pubnub.configuration.subscribeKey, + channel = channels.first(), + options = queryParams + ) + } + + } + + override fun createResponse(input: Response): PNFetchMessagesResult? { + val channelsMap = hashMapOf>() + + for (entry in input.body()!!.channels) { + val items = mutableListOf() + for (item in entry.value) { + items.add( + PNFetchMessageItem( + message = processMessage(item.message), + timetoken = item.timetoken, + meta = item.meta + ).apply { + if (includeMessageActions) { + actions = (item.actions) ?: mapOf() + } + } + ) + } + channelsMap[entry.key] = items + } + + return PNFetchMessagesResult(channelsMap) + } + + private fun processMessage(message: JsonElement): JsonElement { + if (!pubnub.configuration.isCipherKeyValid()) + return message + + val crypto = Crypto(pubnub.configuration.cipherKey) + + val inputText = + if (pubnub.mapper.isJsonObject(message) && pubnub.mapper.hasField(message, "pn_other")) { + pubnub.mapper.elementToString(message, "pn_other") + } else { + pubnub.mapper.elementToString(message) + } + + val outputText = crypto.decrypt(inputText!!) + + var outputObject = pubnub.mapper.fromJson(outputText, JsonElement::class.java) + + pubnub.mapper.getField(message, "pn_other")?.let { + val objectNode = pubnub.mapper.getAsObject(message) + pubnub.mapper.putOnObject(objectNode, "pn_other", outputObject) + outputObject = objectNode + } + + return outputObject + } + + override fun operationType() = PNOperationType.PNFetchMessagesOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/History.kt b/src/main/kotlin/com/pubnub/api/endpoints/History.kt new file mode 100644 index 000000000..dba53aff7 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/History.kt @@ -0,0 +1,142 @@ +package com.pubnub.api.endpoints + +import com.google.gson.JsonElement +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNHistoryItemResult +import com.pubnub.api.models.consumer.history.PNHistoryResult +import com.pubnub.api.vendor.Crypto +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class History(pubnub: PubNub) : Endpoint(pubnub) { + + private companion object { + private const val MAX_COUNT = 100 + } + + lateinit var channel: String + var start: Long? = null + var end: Long? = null + var count = MAX_COUNT + var reverse = false + var includeTimetoken = false + var includeMeta = false + + override fun validateParams() { + super.validateParams() + if (!::channel.isInitialized || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + count = + when (count) { + in 1..MAX_COUNT -> count + else -> MAX_COUNT + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call { + queryParams["reverse"] = reverse.toString() + queryParams["include_token"] = includeTimetoken.toString() + queryParams["include_meta"] = includeMeta.toString() + queryParams["count"] = count.toString() + + start?.let { + queryParams["start"] = it.toString().toLowerCase(Locale.US) + } + end?.let { + queryParams["end"] = it.toString().toLowerCase(Locale.US) + } + + return pubnub.retrofitManager.historyService.fetchHistory( + pubnub.configuration.subscribeKey, + channel, + queryParams + ) + } + + override fun createResponse(input: Response): PNHistoryResult? { + val startTimeToken = pubnub.mapper.elementToLong(pubnub.mapper.getArrayElement(input.body()!!, 1)) + val endTimeToken = pubnub.mapper.elementToLong(pubnub.mapper.getArrayElement(input.body()!!, 2)) + + val messages = mutableListOf() + + var historyData = PNHistoryResult( + messages = messages, + startTimetoken = startTimeToken, + endTimetoken = endTimeToken + ) + + if (pubnub.mapper.getArrayElement(input.body()!!, 0).isJsonArray) { + val iterator = pubnub.mapper.getArrayIterator(pubnub.mapper.getArrayElement(input.body()!!, 0))!! + while (iterator.hasNext()) { + + val historyEntry = iterator.next() + + var message: JsonElement + var timetoken: Long? = null + var meta: JsonElement? = null + + if (includeTimetoken || includeMeta) { + message = processMessage(pubnub.mapper.getField(historyEntry, "message")!!) + if (includeTimetoken) { + timetoken = pubnub.mapper.elementToLong(historyEntry, "timetoken") + } + if (includeMeta) { + meta = pubnub.mapper.getField(historyEntry, "meta") + } + } else { + message = processMessage(historyEntry) + } + + val historyItem = PNHistoryItemResult( + entry = message, + timetoken = timetoken, + meta = meta + ) + + messages.add(historyItem) + } + } else { + throw PubNubException(PubNubError.HTTP_ERROR).apply { + errorMessage = "History is disabled" + } + } + + return historyData + } + + private fun processMessage(message: JsonElement): JsonElement { + if (!pubnub.configuration.isCipherKeyValid()) + return message + + val crypto = Crypto(pubnub.configuration.cipherKey) + + val inputText = + if (pubnub.mapper.isJsonObject(message) && pubnub.mapper.hasField(message, "pn_other")) { + pubnub.mapper.elementToString(message, "pn_other") + } else { + pubnub.mapper.elementToString(message) + } + + val outputText = crypto.decrypt(inputText!!) + + var outputObject = pubnub.mapper.fromJson(outputText, JsonElement::class.java) + + pubnub.mapper.getField(message, "pn_other")?.let { + val objectNode = pubnub.mapper.getAsObject(message) + pubnub.mapper.putOnObject(objectNode, "pn_other", outputObject) + outputObject = objectNode + } + + return outputObject + } + + override fun operationType() = PNOperationType.PNHistoryOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt b/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt new file mode 100644 index 000000000..9d92672f1 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/MessageCounts.kt @@ -0,0 +1,57 @@ +package com.pubnub.api.endpoints + +import com.google.gson.JsonElement +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNMessageCountResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class MessageCounts(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channels: List + lateinit var channelsTimetoken: List + + override fun validateParams() { + super.validateParams() + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (!::channelsTimetoken.isInitialized || channelsTimetoken.isEmpty()) { + throw PubNubException(PubNubError.TIMETOKEN_MISSING) + } + if (channelsTimetoken.size != channels.size && channelsTimetoken.size > 1) { + throw PubNubException(PubNubError.CHANNELS_TIMETOKEN_MISMATCH) + } + } + + override fun getAffectedChannels() = channels + + override fun doWork(queryParams: HashMap): Call { + if (channelsTimetoken.size == 1) { + queryParams["timetoken"] = channelsTimetoken.toCsv() + } else { + queryParams["channelsTimetoken"] = channelsTimetoken.toCsv() + } + + return pubnub.retrofitManager.historyService.fetchCount( + subKey = pubnub.configuration.subscribeKey, + channels = channels.toCsv(), + options = queryParams + ) + } + + override fun createResponse(input: Response): PNMessageCountResult? { + val channelsMap = HashMap() + + val it = pubnub.mapper.getObjectIterator(input.body()!!, "channels") + while (it.hasNext()) { + val entry = it.next() + channelsMap[entry.key] = entry.value.asLong + } + return PNMessageCountResult(channelsMap) + } + + override fun operationType() = PNOperationType.PNMessageCountOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/Time.kt b/src/main/kotlin/com/pubnub/api/endpoints/Time.kt new file mode 100644 index 000000000..9d3886d0d --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/Time.kt @@ -0,0 +1,27 @@ +package com.pubnub.api.endpoints + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNTimeResult +import retrofit2.Response + +class Time(pubnub: PubNub) : Endpoint, PNTimeResult>(pubnub) { + + override fun getAffectedChannels() = emptyList() + + override fun getAffectedChannelGroups() = emptyList() + + override fun doWork(queryParams: HashMap) = + pubnub.retrofitManager.timeService.fetchTime(queryParams) + + override fun createResponse(input: Response>): PNTimeResult? { + return PNTimeResult(input.body()!![0]) + } + + override fun operationType() = PNOperationType.PNTimeOperation + + override fun isAuthRequired() = false + override fun isSubKeyRequired() = false + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt b/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt new file mode 100644 index 000000000..316feb54e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/access/Grant.kt @@ -0,0 +1,117 @@ +package com.pubnub.api.endpoints.access + +import com.google.gson.JsonElement +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerGrantResult +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerKeyData +import com.pubnub.api.models.server.Envelope +import com.pubnub.api.models.server.access_manager.AccessManagerGrantPayload +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class Grant(pubnub: PubNub) : Endpoint, PNAccessManagerGrantResult>(pubnub) { + + var read = false + var write = false + var manage = false + var delete = false + var ttl: Int = -1 + + var authKeys = emptyList() + var channels = emptyList() + var channelGroups = emptyList() + + override fun validateParams() { + super.validateParams() + if (!pubnub.configuration.isSecretKeyValid()) { + throw PubNubException(PubNubError.SECRET_KEY_MISSING) + } + } + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = channelGroups + + override fun doWork(queryParams: HashMap): Call> { + channels.run { + if (isNotEmpty()) queryParams["channel"] = toCsv() + } + channelGroups.run { + if (isNotEmpty()) queryParams["channel-group"] = toCsv() + } + authKeys.run { + if (isNotEmpty()) queryParams["auth"] = toCsv() + } + + if (ttl >= -1) { + queryParams["ttl"] = ttl.toString() + } + + queryParams["r"] = if (read) "1" else "0" + queryParams["w"] = if (write) "1" else "0" + queryParams["m"] = if (manage) "1" else "0" + queryParams["d"] = if (delete) "1" else "0" + + return pubnub.retrofitManager.accessManagerService + .grant( + subKey = pubnub.configuration.subscribeKey, + options = queryParams + ) + } + + override fun createResponse(input: Response>): PNAccessManagerGrantResult? { + val data = input.body()!!.payload!! + + val constructedChannels = mutableMapOf?>() + val constructedGroups = mutableMapOf?>() + + // we have a case of a singular channel. + data.channel?.let { + constructedChannels[it] = data.authKeys!! + } + + if (channelGroups.size == 1) { + constructedGroups[pubnub.mapper.elementToString(data.channelGroups)!!] = data.authKeys!! + } else if (channelGroups.size > 1) { + val it = pubnub.mapper.getObjectIterator(data.channelGroups!!) + while (it.hasNext()) { + val (k, v) = it.next() + constructedGroups[k] = createKeyMap(v) + } + } + + data.channels?.forEach { + constructedChannels[it.key] = data.channels[it.key]!!.authKeys + } + + return PNAccessManagerGrantResult( + level = data.level!!, + ttl = data.ttl, + subscribeKey = data.subscribeKey!!, + channels = constructedChannels, + channelGroups = constructedGroups + ) + } + + private fun createKeyMap(input: JsonElement): Map { + val result: MutableMap = + HashMap() + val it: Iterator> = + pubnub.mapper.getObjectIterator(input, "auths") + while (it.hasNext()) { + val keyMap = it.next() + val pnAccessManagerKeyData = PNAccessManagerKeyData() + pnAccessManagerKeyData.manageEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "m")) + pnAccessManagerKeyData.writeEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "w")) + pnAccessManagerKeyData.readEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "r")) + pnAccessManagerKeyData.deleteEnabled = (pubnub.mapper.getAsBoolean(keyMap.value, "d")) + result[keyMap.key] = pnAccessManagerKeyData + } + return result + } + + override fun operationType() = PNOperationType.PNAccessManagerGrant + + override fun isAuthRequired() = false +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt new file mode 100644 index 000000000..d6945b0b7 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AddChannelChannelGroup.kt @@ -0,0 +1,46 @@ +package com.pubnub.api.endpoints.channel_groups + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAddChannelResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class AddChannelChannelGroup(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channelGroup: String + lateinit var channels: List + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = listOf(channelGroup) + + override fun validateParams() { + super.validateParams() + if (!::channelGroup.isInitialized || channelGroup.isBlank()) { + throw PubNubException(PubNubError.GROUP_MISSING) + } + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call { + if (channels.isNotEmpty()) { + queryParams["add"] = channels.toCsv() + } + + return pubnub.retrofitManager.channelGroupService + .addChannelChannelGroup( + pubnub.configuration.subscribeKey, + channelGroup, + queryParams + ) + } + + override fun createResponse(input: Response): PNChannelGroupsAddChannelResult? { + return PNChannelGroupsAddChannelResult() + } + + override fun operationType() = PNOperationType.PNAddChannelsToGroupOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt new file mode 100644 index 000000000..e68448f52 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/AllChannelsChannelGroup.kt @@ -0,0 +1,44 @@ +package com.pubnub.api.endpoints.channel_groups + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAllChannelsResult +import com.pubnub.api.models.server.Envelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class AllChannelsChannelGroup(pubnub: PubNub) : + Endpoint>, PNChannelGroupsAllChannelsResult>(pubnub) { + + lateinit var channelGroup: String + + override fun getAffectedChannelGroups() = listOf(channelGroup) + + override fun validateParams() { + super.validateParams() + if (!::channelGroup.isInitialized || channelGroup.isBlank()) { + throw PubNubException(PubNubError.GROUP_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call>> { + return pubnub.retrofitManager.channelGroupService + .allChannelsChannelGroup( + pubnub.configuration.subscribeKey, + channelGroup, + queryParams + ) + } + + override fun createResponse(input: Response>>): PNChannelGroupsAllChannelsResult? { + return PNChannelGroupsAllChannelsResult( + input.body()!!.payload!!["channels"] as List + ) + } + + override fun operationType() = PNOperationType.PNChannelsForGroupOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt new file mode 100644 index 000000000..f5203f33c --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/DeleteChannelGroup.kt @@ -0,0 +1,40 @@ +package com.pubnub.api.endpoints.channel_groups + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsDeleteGroupResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class DeleteChannelGroup(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channelGroup: String + + override fun validateParams() { + super.validateParams() + if (!::channelGroup.isInitialized || channelGroup.isBlank()) { + throw PubNubException(PubNubError.GROUP_MISSING) + } + } + + override fun getAffectedChannelGroups() = listOf(channelGroup) + + override fun doWork(queryParams: HashMap): Call { + return pubnub.retrofitManager.channelGroupService + .deleteChannelGroup( + pubnub.configuration.subscribeKey, + channelGroup, + queryParams + ) + } + + override fun createResponse(input: Response): PNChannelGroupsDeleteGroupResult? { + return PNChannelGroupsDeleteGroupResult() + } + + override fun operationType() = PNOperationType.PNRemoveGroupOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt new file mode 100644 index 000000000..f993a7366 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/ListAllChannelGroup.kt @@ -0,0 +1,29 @@ +package com.pubnub.api.endpoints.channel_groups + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsListAllResult +import com.pubnub.api.models.server.Envelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class ListAllChannelGroup(pubnub: PubNub) : Endpoint>, PNChannelGroupsListAllResult>(pubnub) { + + override fun doWork(queryParams: HashMap): Call>> { + return pubnub.retrofitManager.channelGroupService + .listAllChannelGroup( + pubnub.configuration.subscribeKey, + queryParams + ) + } + + override fun createResponse(input: Response>>): PNChannelGroupsListAllResult? { + return PNChannelGroupsListAllResult( + input.body()!!.payload!!["groups"] as List + ) + } + + override fun operationType() = PNOperationType.PNChannelGroupsOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt new file mode 100644 index 000000000..494a488da --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/channel_groups/RemoveChannelChannelGroup.kt @@ -0,0 +1,47 @@ +package com.pubnub.api.endpoints.channel_groups + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsRemoveChannelResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class RemoveChannelChannelGroup(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channelGroup: String + lateinit var channels: List + + override fun getAffectedChannels() = channels + + override fun getAffectedChannelGroups() = listOf(channelGroup) + + override fun validateParams() { + super.validateParams() + if (!::channelGroup.isInitialized || channelGroup.isBlank()) { + throw PubNubException(PubNubError.GROUP_MISSING) + } + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call { + if (channels.isNotEmpty()) { + queryParams["remove"] = channels.toCsv() + } + + return pubnub.retrofitManager.channelGroupService + .removeChannel( + pubnub.configuration.subscribeKey, + channelGroup, + queryParams + ) + } + + override fun createResponse(input: Response): PNChannelGroupsRemoveChannelResult? { + return PNChannelGroupsRemoveChannelResult() + } + + override fun operationType() = PNOperationType.PNRemoveChannelsFromGroupOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt new file mode 100644 index 000000000..876e78e8a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/AddMessageAction.kt @@ -0,0 +1,59 @@ +package com.pubnub.api.endpoints.message_actions + +import com.google.gson.JsonObject +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNAddMessageActionResult +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.server.objects_api.EntityEnvelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class AddMessageAction(pubnub: PubNub) : Endpoint, PNAddMessageActionResult>(pubnub) { + + lateinit var channel: String + lateinit var messageAction: PNMessageAction + + override fun validateParams() { + super.validateParams() + if (!::channel.isInitialized || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (!::messageAction.isInitialized) { + throw PubNubException(PubNubError.MESSAGE_ACTION_MISSING) + } + if (messageAction.type.isBlank()) { + throw PubNubException(PubNubError.MESSAGE_ACTION_TYPE_MISSING) + } + if (messageAction.value.isBlank()) { + throw PubNubException(PubNubError.MESSAGE_ACTION_VALUE_MISSING) + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call> { + val body = JsonObject() + body.addProperty("type", messageAction.type) + body.addProperty("value", messageAction.value) + + return pubnub.retrofitManager.messageActionService + .addMessageAction( + subKey = pubnub.configuration.subscribeKey, + channel = channel, + messageTimetoken = messageAction.messageTimetoken.toString().toLowerCase(), + body = body, + options = queryParams + ) + } + + override fun createResponse(input: Response>): PNAddMessageActionResult? { + return PNAddMessageActionResult(input.body()!!.data!!) + } + + override fun operationType() = PNOperationType.PNAddMessageAction +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt new file mode 100644 index 000000000..713f678e6 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/GetMessageActions.kt @@ -0,0 +1,51 @@ +package com.pubnub.api.endpoints.message_actions + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNGetMessageActionsResult +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.server.objects_api.EntityEnvelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class GetMessageActions(pubnub: PubNub) : + Endpoint>, PNGetMessageActionsResult>(pubnub) { + + lateinit var channel: String + var start: Long? = null + var end: Long? = null + var limit: Int? = null + + override fun validateParams() { + super.validateParams() + if (!::channel.isInitialized || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call>> { + + start?.let { queryParams["start"] = it.toString().toLowerCase() } + end?.let { queryParams["end"] = it.toString().toLowerCase() } + limit?.let { queryParams["limit"] = it.toString().toLowerCase() } + + return pubnub.retrofitManager.messageActionService + .getMessageActions( + pubnub.configuration.subscribeKey, + channel, + queryParams + ) + } + + override fun createResponse(input: Response>>): PNGetMessageActionsResult? { + return PNGetMessageActionsResult(input.body()!!.data!!) + } + + override fun operationType() = PNOperationType.PNGetMessageActions +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt new file mode 100644 index 000000000..98fb42ce3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/message_actions/RemoveMessageAction.kt @@ -0,0 +1,50 @@ +package com.pubnub.api.endpoints.message_actions + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class RemoveMessageAction(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var channel: String + var messageTimetoken: Long? = null + var actionTimetoken: Long? = null + + override fun validateParams() { + super.validateParams() + if (!::channel.isInitialized || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (messageTimetoken == null) { + throw PubNubException(PubNubError.MESSAGE_TIMETOKEN_MISSING) + } + if (actionTimetoken == null) { + throw PubNubException(PubNubError.MESSAGE_ACTION_TIMETOKEN_MISSING) + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call { + return pubnub.retrofitManager.messageActionService + .deleteMessageAction( + subKey = pubnub.configuration.subscribeKey, + channel = channel, + messageTimetoken = messageTimetoken.toString().toLowerCase(), + actionTimetoken = actionTimetoken.toString().toLowerCase(), + options = queryParams + ) + } + + override fun createResponse(input: Response): PNRemoveMessageActionResult? { + return PNRemoveMessageActionResult() + } + + override fun operationType() = PNOperationType.PNDeleteMessageAction +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt new file mode 100644 index 000000000..bf00f10e1 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/GetState.kt @@ -0,0 +1,61 @@ +package com.pubnub.api.endpoints.presence + +import com.google.gson.JsonElement +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNGetStateResult +import com.pubnub.api.models.server.Envelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class GetState(pubnub: PubNub) : Endpoint, PNGetStateResult>(pubnub) { + + var channels = listOf() + var channelGroups = listOf() + var uuid = pubnub.configuration.uuid + + override fun getAffectedChannels() = channels + + override fun getAffectedChannelGroups() = channelGroups + + override fun validateParams() { + super.validateParams() + if (channels.isNullOrEmpty() && channelGroups.isNullOrEmpty()) { + throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call> { + if (channelGroups.isNotEmpty()) { + queryParams["channel-group"] = channelGroups.toCsv() + } + + return pubnub.retrofitManager.presenceService.getState( + pubnub.configuration.subscribeKey, + channels.toCsv(), + uuid, + queryParams + ) + } + + override fun createResponse(input: Response>): PNGetStateResult? { + val stateMappings = hashMapOf() + if (channels.size == 1 && channelGroups.isEmpty()) { + stateMappings[channels.first()] = input.body()!!.payload!! + } else { + val it = pubnub.mapper.getObjectIterator(input.body()!!.payload!!) + while (it.hasNext()) { + val stateMapping = it.next() + stateMappings[stateMapping.key] = stateMapping.value + } + } + + return PNGetStateResult(stateMappings) + } + + override fun operationType() = PNOperationType.PNGetState +} + + + diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt new file mode 100644 index 000000000..0e365196a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/Heartbeat.kt @@ -0,0 +1,54 @@ +package com.pubnub.api.endpoints.presence + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class Heartbeat internal constructor( + pubnub: PubNub, + val channels: List = listOf(), + val channelGroups: List = listOf() +) : Endpoint(pubnub) { + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = channelGroups + + override fun validateParams() { + super.validateParams() + if (channels.isEmpty() && channelGroups.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call { + queryParams["heartbeat"] = pubnub.configuration.presenceTimeout.toString() + + if (channelGroups.isNotEmpty()) { + queryParams["channel-group"] = channelGroups.joinToString(",") + } + + val channelsCsv = + if (channels.isNotEmpty()) + channels.joinToString(",") + else + "," + + return pubnub.retrofitManager.presenceService.heartbeat( + pubnub.configuration.subscribeKey, + channelsCsv, + queryParams + ) + } + + override fun createResponse(input: Response): Boolean? { + return true + } + + override fun operationType() = PNOperationType.PNHeartbeatOperation + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt new file mode 100644 index 000000000..6c5d9b6e5 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt @@ -0,0 +1,131 @@ +package com.pubnub.api.endpoints.presence + +import com.google.gson.JsonElement +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNHereNowChannelData +import com.pubnub.api.models.consumer.presence.PNHereNowOccupantData +import com.pubnub.api.models.consumer.presence.PNHereNowResult +import com.pubnub.api.models.server.Envelope +import com.pubnub.api.toCsv +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class HereNow(pubnub: PubNub) : Endpoint, PNHereNowResult>(pubnub) { + + var channels = listOf() + var channelGroups = listOf() + var includeState = false + var includeUUIDs = true + + private fun isGlobalHereNow() = channels.isEmpty() && channelGroups.isEmpty() + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = channelGroups + + override fun doWork(queryParams: HashMap): Call> { + if (includeState) { + queryParams["state"] = "1" + } + if (!includeUUIDs) { + queryParams["disable_uuids"] = "1" + } + if (channelGroups.isNotEmpty()) { + queryParams["channel-group"] = channelGroups.toCsv() + } + + return if (!isGlobalHereNow()) { + pubnub.retrofitManager.presenceService.hereNow( + pubnub.configuration.subscribeKey, + channels.toCsv(), + queryParams + ) + } else { + pubnub.retrofitManager.presenceService.globalHereNow( + pubnub.configuration.subscribeKey, + queryParams + ) + } + } + + override fun createResponse(input: Response>): PNHereNowResult? { + return if (isGlobalHereNow()) { + parseMultipleChannelResponse(input.body()?.payload!!) + } else { + if (channels.size > 1 || channelGroups.isNotEmpty()) { + parseMultipleChannelResponse(input.body()?.payload!!) + } else { + parseSingleChannelResponse(input.body()!!) + } + } + } + + private fun parseSingleChannelResponse(input: Envelope): PNHereNowResult { + val pnHereNowResult = PNHereNowResult( + totalChannels = 1, + totalOccupancy = input.occupancy + ) + + val pnHereNowChannelData = PNHereNowChannelData( + channelName = channels[0], + occupancy = input.occupancy + ) + + if (includeUUIDs) { + pnHereNowChannelData.occupants = prepareOccupantData(input.uuids!!) + pnHereNowResult.channels[channels[0]] = pnHereNowChannelData + } + + return pnHereNowResult + } + + private fun parseMultipleChannelResponse(input: JsonElement): PNHereNowResult { + val pnHereNowResult = PNHereNowResult( + totalChannels = pubnub.mapper.elementToInt(input, "total_channels"), + totalOccupancy = pubnub.mapper.elementToInt(input, "total_occupancy") + ) + + val it = pubnub.mapper.getObjectIterator(input, "channels") + + while (it.hasNext()) { + val entry = it.next() + val pnHereNowChannelData = PNHereNowChannelData( + channelName = entry.key, + occupancy = pubnub.mapper.elementToInt(entry.value, "occupancy") + ) + if (includeUUIDs) { + pnHereNowChannelData.occupants = prepareOccupantData(pubnub.mapper.getField(entry.value, "uuids")!!) + } + pnHereNowResult.channels[entry.key] = pnHereNowChannelData + } + + return pnHereNowResult + } + + private fun prepareOccupantData(input: JsonElement): MutableList { + val occupantsResults = mutableListOf() + + val it = pubnub.mapper.getArrayIterator(input) + while (it?.hasNext()!!) { + val occupant = it.next() + occupantsResults.add( + if (includeState) { + PNHereNowOccupantData( + uuid = pubnub.mapper.elementToString(occupant, "uuid")!!, + state = pubnub.mapper.getField(occupant, "state") + ) + } else { + PNHereNowOccupantData( + uuid = pubnub.mapper.elementToString(occupant)!! + ) + } + ) + } + + return occupantsResults + } + + override fun operationType() = PNOperationType.PNHereNowOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt new file mode 100644 index 000000000..57d0e1e05 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/Leave.kt @@ -0,0 +1,39 @@ +package com.pubnub.api.endpoints.presence + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class Leave internal constructor(pubnub: PubNub) : Endpoint(pubnub) { + + var channels = emptyList() + var channelGroups = emptyList() + + override fun validateParams() { + super.validateParams() + if (channels.isEmpty() && channelGroups.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING) + } + } + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = channelGroups + + override fun doWork(queryParams: HashMap): Call { + queryParams["channel-group"] = channelGroups.toCsv() + + return pubnub.retrofitManager.presenceService.leave( + pubnub.configuration.subscribeKey, + channels.toCsv(), + queryParams + ) + } + + override fun createResponse(input: Response) = true + + override fun operationType() = PNOperationType.PNUnsubscribeOperation +} + + diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt new file mode 100644 index 000000000..560923ceb --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/SetState.kt @@ -0,0 +1,70 @@ +package com.pubnub.api.endpoints.presence + +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.pubnub.api.* +import com.pubnub.api.builder.StateOperation +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNSetStateResult +import com.pubnub.api.models.server.Envelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class SetState(pubnub: PubNub) : Endpoint, PNSetStateResult>(pubnub) { + + var channels = emptyList() + var channelGroups = emptyList() + var uuid = pubnub.configuration.uuid + lateinit var state: Any + + override fun getAffectedChannels() = channels + override fun getAffectedChannelGroups() = channelGroups + + override fun validateParams() { + super.validateParams() + if (channels.isNullOrEmpty() && channelGroups.isNullOrEmpty()) { + throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING) + } + if (!::state.isInitialized) { + throw PubNubException(PubNubError.STATE_MISSING) + } + } + + override fun doWork(queryParams: HashMap): Call> { + if (uuid == pubnub.configuration.uuid) { + pubnub.subscriptionManager.adaptStateBuilder( + StateOperation( + state = state + ).apply { + this.channels = this@SetState.channels + this.channelGroups = this@SetState.channelGroups + } + ) + } + + if (channelGroups.isNotEmpty()) { + queryParams["channel-group"] = channelGroups.toCsv() + } + queryParams["state"] = pubnub.mapper.toJson(state) + + return pubnub.retrofitManager.presenceService.setState( + pubnub.configuration.subscribeKey, + channels.toCsv(), + uuid, + queryParams + ) + } + + override fun createResponse(input: Response>): PNSetStateResult? { + if (input.body()!!.payload!! is JsonNull) { + throw PubNubException(PubNubError.PARSING_ERROR) + } + return PNSetStateResult(input.body()!!.payload!!) + } + + override fun operationType() = PNOperationType.PNSetStateOperation +} + + + diff --git a/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt b/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt new file mode 100644 index 000000000..a1099b24d --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/presence/WhereNow.kt @@ -0,0 +1,30 @@ +package com.pubnub.api.endpoints.presence + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNWhereNowResult +import com.pubnub.api.models.server.Envelope +import com.pubnub.api.models.server.presence.WhereNowPayload +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class WhereNow(pubnub: PubNub) : Endpoint, PNWhereNowResult>(pubnub) { + + var uuid = pubnub.configuration.uuid + + override fun doWork(queryParams: HashMap): Call> { + return pubnub.retrofitManager.presenceService.whereNow( + pubnub.configuration.subscribeKey, + uuid, + queryParams + ) + } + + override fun createResponse(input: Response>): PNWhereNowResult? { + return PNWhereNowResult(input.body()!!.payload!!.channels) + } + + override fun operationType() = PNOperationType.PNWhereNowOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt new file mode 100644 index 000000000..c33eca924 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Publish.kt @@ -0,0 +1,102 @@ +package com.pubnub.api.endpoints.pubsub + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.vendor.Crypto +import retrofit2.Call +import retrofit2.Response + +class Publish(pubnub: PubNub) : Endpoint, PNPublishResult>(pubnub) { + + lateinit var channel: String + lateinit var message: Any + lateinit var meta: Any + var shouldStore: Boolean? = null + var usePost = false + var replicate = true + var ttl: Int? = null + + private fun isChannelValid() = ::channel.isInitialized + private fun isMessageValid() = ::message.isInitialized + private fun isMetaValid() = ::meta.isInitialized + + override fun validateParams() { + super.validateParams() + if (!isChannelValid() || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (!isMessageValid()) { + throw PubNubException(PubNubError.MESSAGE_MISSING) + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call> { + + var stringifiedMessage = pubnub.mapper.toJson(message) + + if (isMetaValid()) { + queryParams["meta"] = pubnub.mapper.toJson(meta) + } + + shouldStore?.run { queryParams["store"] = if (this) "1" else "0" } + + ttl?.let { queryParams["ttl"] = it.toString() } + + if (!replicate) queryParams["norep"] = "true" + + queryParams["seqn"] = pubnub.publishSequenceManager.nextSequence().toString() + + if (pubnub.configuration.isCipherKeyValid()) { + stringifiedMessage = Crypto(pubnub.configuration.cipherKey) + .encrypt(stringifiedMessage) + .replace("\n", "") + } + + if (usePost) { + var payload = message + + if (pubnub.configuration.isCipherKeyValid()) { + payload = stringifiedMessage + } + + return pubnub.retrofitManager.publishService.publishWithPost( + pubnub.configuration.publishKey, + pubnub.configuration.subscribeKey, + channel, + payload, + queryParams + ) + } else { + // get request + + if (pubnub.configuration.isCipherKeyValid()) { + stringifiedMessage = "\"$stringifiedMessage\"" + } + + return pubnub.retrofitManager.publishService.publish( + pubnub.configuration.publishKey, + pubnub.configuration.subscribeKey, + channel, + stringifiedMessage, + queryParams + ) + } + } + + override fun createResponse(input: Response>): PNPublishResult? { + return PNPublishResult( + timetoken = input.body()!![2].toString().toLong() + ) + } + + override fun operationType() = PNOperationType.PNPublishOperation + + override fun isPubKeyRequired() = true + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt new file mode 100644 index 000000000..87ec1ee67 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Signal.kt @@ -0,0 +1,52 @@ +package com.pubnub.api.endpoints.pubsub + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNPublishResult +import retrofit2.Call +import retrofit2.Response + +class Signal(pubnub: PubNub) : Endpoint, PNPublishResult>(pubnub) { + + lateinit var channel: String + lateinit var message: Any + + private fun isChannelValid() = ::channel.isInitialized + private fun isMessageValid() = ::message.isInitialized + + override fun validateParams() { + super.validateParams() + if (!isChannelValid() || channel.isBlank()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (!isMessageValid()) { + throw PubNubException(PubNubError.MESSAGE_MISSING) + } + } + + override fun getAffectedChannels() = listOf(channel) + + override fun doWork(queryParams: HashMap): Call> { + return pubnub.retrofitManager.signalService.signal( + pubKey = pubnub.configuration.publishKey, + subKey = pubnub.configuration.subscribeKey, + channel = channel, + message = pubnub.mapper.toJson(message), + options = queryParams + ) + } + + override fun createResponse(input: Response>): PNPublishResult? { + return PNPublishResult( + timetoken = input.body()!![2].toString().toLong() + ) + } + + override fun operationType() = PNOperationType.PNSignalOperation + + override fun isPubKeyRequired() = true + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt new file mode 100644 index 000000000..7c61942eb --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/pubsub/Subscribe.kt @@ -0,0 +1,65 @@ +package com.pubnub.api.endpoints.pubsub + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.server.SubscribeEnvelope +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class Subscribe internal constructor(pubnub: PubNub) : Endpoint(pubnub) { + + var channels = emptyList() + var channelGroups = emptyList() + var timetoken: Long? = null + var region: String? = null + var state: Any? = null + var filterExpression: String? = null + + override fun validateParams() { + super.validateParams() + if (channels.isEmpty() && channelGroups.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_AND_GROUP_MISSING) + } + } + + override fun getAffectedChannels() = channels + + override fun getAffectedChannelGroups() = channelGroups + + override fun doWork(queryParams: HashMap): Call { + if (channelGroups.isNotEmpty()) { + queryParams["channel-group"] = channelGroups.joinToString(",") + } + + if (!filterExpression.isNullOrBlank()) { + queryParams["filter-expr"] = filterExpression!! + } + + timetoken?.let { + queryParams["tt"] = it.toString() + } + + region?.let { + queryParams["tr"] = it + } + + queryParams["heartbeat"] = pubnub.configuration.presenceTimeout.toString() + + state?.let { + queryParams["state"] = pubnub.mapper.toJson(it) + } + + return pubnub.retrofitManager.subscribeService.subscribe( + pubnub.configuration.subscribeKey, + channels.toCsv(), + queryParams + ) + } + + override fun createResponse(input: Response): SubscribeEnvelope? { + return input.body()!! + } + + override fun operationType() = PNOperationType.PNSubscribeOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt new file mode 100644 index 000000000..31063213b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/push/AddChannelsToPush.kt @@ -0,0 +1,71 @@ +package com.pubnub.api.endpoints.push + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushAddChannelResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class AddChannelsToPush(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var pushType: PNPushType + lateinit var channels: List + lateinit var deviceId: String + var environment = PNPushEnvironment.DEVELOPMENT + lateinit var topic: String + + override fun getAffectedChannels() = channels + + override fun validateParams() { + super.validateParams() + if (!::pushType.isInitialized) { + throw PubNubException(PubNubError.PUSH_TYPE_MISSING) + } + if (!::deviceId.isInitialized || deviceId.isBlank()) { + throw PubNubException(PubNubError.DEVICE_ID_MISSING) + } + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (pushType == PNPushType.APNS2) { + if (!::topic.isInitialized || topic.isBlank()) { + throw PubNubException(PubNubError.PUSH_TOPIC_MISSING) + } + } + } + + override fun doWork(queryParams: HashMap): Call { + queryParams["add"] = channels.toCsv() + + if (pushType != PNPushType.APNS2) { + queryParams["type"] = pushType.toParamString() + + return pubnub.retrofitManager.pushService + .modifyChannelsForDevice( + subKey = pubnub.configuration.subscribeKey, + pushToken = deviceId, + options = queryParams + ) + } + + queryParams["environment"] = environment.name.toLowerCase() + queryParams["topic"] = topic + + return pubnub.retrofitManager.pushService + .modifyChannelsForDeviceApns2( + subKey = pubnub.configuration.subscribeKey, + deviceApns2 = deviceId, + options = queryParams + ) + + } + + override fun createResponse(input: Response): PNPushAddChannelResult? { + return PNPushAddChannelResult() + } + + override fun operationType() = PNOperationType.PNAddPushNotificationsOnChannelsOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt new file mode 100644 index 000000000..04142acc3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/push/ListPushProvisions.kt @@ -0,0 +1,66 @@ +package com.pubnub.api.endpoints.push + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushListProvisionsResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class ListPushProvisions(pubnub: PubNub) : Endpoint, PNPushListProvisionsResult>(pubnub) { + + lateinit var pushType: PNPushType + lateinit var deviceId: String + var environment = PNPushEnvironment.DEVELOPMENT + lateinit var topic: String + + override fun validateParams() { + super.validateParams() + if (!::pushType.isInitialized) { + throw PubNubException(PubNubError.PUSH_TYPE_MISSING) + } + if (!::deviceId.isInitialized || deviceId.isBlank()) { + throw PubNubException(PubNubError.DEVICE_ID_MISSING) + } + if (pushType == PNPushType.APNS2) { + if (!::topic.isInitialized || topic.isBlank()) { + throw PubNubException(PubNubError.PUSH_TOPIC_MISSING) + } + } + } + + override fun doWork(queryParams: HashMap): Call> { + if (pushType != PNPushType.APNS2) { + queryParams["type"] = pushType.toParamString() + + return pubnub.retrofitManager.pushService + .listChannelsForDevice( + subKey = pubnub.configuration.subscribeKey, + pushToken = deviceId, + options = queryParams + ) + } + + queryParams["environment"] = environment.name.toLowerCase() + queryParams["topic"] = topic + + return pubnub.retrofitManager.pushService + .listChannelsForDeviceApns2( + subKey = pubnub.configuration.subscribeKey, + deviceApns2 = deviceId, + options = queryParams + ) + + } + + override fun createResponse(input: Response>): PNPushListProvisionsResult? { + return PNPushListProvisionsResult(input.body()!!) + } + + override fun operationType() = PNOperationType.PNPushNotificationEnabledChannelsOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt new file mode 100644 index 000000000..99832820a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveAllPushChannelsForDevice.kt @@ -0,0 +1,66 @@ +package com.pubnub.api.endpoints.push + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveAllChannelsResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class RemoveAllPushChannelsForDevice(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var pushType: PNPushType + lateinit var deviceId: String + var environment = PNPushEnvironment.DEVELOPMENT + lateinit var topic: String + + override fun validateParams() { + super.validateParams() + if (!::pushType.isInitialized) { + throw PubNubException(PubNubError.PUSH_TYPE_MISSING) + } + if (!::deviceId.isInitialized || deviceId.isBlank()) { + throw PubNubException(PubNubError.DEVICE_ID_MISSING) + } + if (pushType == PNPushType.APNS2) { + if (!::topic.isInitialized || topic.isBlank()) { + throw PubNubException(PubNubError.PUSH_TOPIC_MISSING) + } + } + } + + override fun doWork(queryParams: HashMap): Call { + if (pushType != PNPushType.APNS2) { + queryParams["type"] = pushType.toParamString() + + return pubnub.retrofitManager.pushService + .removeAllChannelsForDevice( + subKey = pubnub.configuration.subscribeKey, + pushToken = deviceId, + options = queryParams + ) + } + + queryParams["environment"] = environment.name.toLowerCase() + queryParams["topic"] = topic + + return pubnub.retrofitManager.pushService + .removeAllChannelsForDeviceApns2( + subKey = pubnub.configuration.subscribeKey, + deviceApns2 = deviceId, + options = queryParams + ) + + } + + override fun createResponse(input: Response): PNPushRemoveAllChannelsResult? { + return PNPushRemoveAllChannelsResult() + } + + override fun operationType() = PNOperationType.PNRemoveAllPushNotificationsOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt new file mode 100644 index 000000000..71d9ea98b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/endpoints/push/RemoveChannelsFromPush.kt @@ -0,0 +1,71 @@ +package com.pubnub.api.endpoints.push + +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult +import retrofit2.Call +import retrofit2.Response +import java.util.* + +class RemoveChannelsFromPush(pubnub: PubNub) : Endpoint(pubnub) { + + lateinit var pushType: PNPushType + lateinit var channels: List + lateinit var deviceId: String + var environment = PNPushEnvironment.DEVELOPMENT + lateinit var topic: String + + override fun getAffectedChannels() = channels + + override fun validateParams() { + super.validateParams() + if (!::pushType.isInitialized) { + throw PubNubException(PubNubError.PUSH_TYPE_MISSING) + } + if (!::deviceId.isInitialized || deviceId.isBlank()) { + throw PubNubException(PubNubError.DEVICE_ID_MISSING) + } + if (!::channels.isInitialized || channels.isEmpty()) { + throw PubNubException(PubNubError.CHANNEL_MISSING) + } + if (pushType == PNPushType.APNS2) { + if (!::topic.isInitialized || topic.isBlank()) { + throw PubNubException(PubNubError.PUSH_TOPIC_MISSING) + } + } + } + + override fun doWork(queryParams: HashMap): Call { + queryParams["remove"] = channels.toCsv() + + if (pushType != PNPushType.APNS2) { + queryParams["type"] = pushType.toParamString() + + return pubnub.retrofitManager.pushService + .modifyChannelsForDevice( + subKey = pubnub.configuration.subscribeKey, + pushToken = deviceId, + options = queryParams + ) + } + + queryParams["environment"] = environment.name.toLowerCase() + queryParams["topic"] = topic + + return pubnub.retrofitManager.pushService + .modifyChannelsForDeviceApns2( + subKey = pubnub.configuration.subscribeKey, + deviceApns2 = deviceId, + options = queryParams + ) + + } + + override fun createResponse(input: Response): PNPushRemoveChannelResult? { + return PNPushRemoveChannelResult() + } + + override fun operationType() = PNOperationType.PNRemovePushNotificationsFromChannelsOperation +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt b/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt new file mode 100644 index 000000000..322145904 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNHeartbeatNotificationOptions.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.enums + +enum class PNHeartbeatNotificationOptions { + NONE, + FAILURES, + ALL +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt b/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt new file mode 100644 index 000000000..583a0e34b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNLogVerbosity.kt @@ -0,0 +1,6 @@ +package com.pubnub.api.enums + +enum class PNLogVerbosity { + NONE, + BODY, +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt b/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt new file mode 100644 index 000000000..6b7b7fadf --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNOperationType.kt @@ -0,0 +1,78 @@ +package com.pubnub.api.enums + +sealed class PNOperationType(open val queryParam: String? = null) { + + open class PublishOperation : PNOperationType("pub") + open class HistoryOperation : PNOperationType("hist") + open class PresenceOperation : PNOperationType("pres") + open class ChannelGroupOperation : PNOperationType("cg") + open class PushNotificationsOperation : PNOperationType("push") + open class PAMOperation : PNOperationType("pam") + open class MessageCountsOperation : PNOperationType("mc") + open class SignalsOperation : PNOperationType("sig") + open class ObjectsOperation : PNOperationType("obj") + open class PAMV3Operation : PNOperationType("pamv3") + open class MessageActionsOperation : PNOperationType("msga") + open class TimeOperation : PNOperationType("time") + + object PNSubscribeOperation : PNOperationType() + + object PNPublishOperation : PublishOperation() + + object PNHistoryOperation : HistoryOperation() + object PNFetchMessagesOperation : HistoryOperation() + object PNDeleteMessagesOperation : HistoryOperation() + + object PNUnsubscribeOperation : PresenceOperation() + object PNWhereNowOperation : PresenceOperation() + object PNHereNowOperation : PresenceOperation() + object PNHeartbeatOperation : PresenceOperation() + object PNSetStateOperation : PresenceOperation() + object PNGetState : PresenceOperation() + + object PNAddChannelsToGroupOperation : ChannelGroupOperation() + object PNRemoveChannelsFromGroupOperation : ChannelGroupOperation() + object PNChannelGroupsOperation : ChannelGroupOperation() + object PNRemoveGroupOperation : ChannelGroupOperation() + object PNChannelsForGroupOperation : ChannelGroupOperation() + + object PNPushNotificationEnabledChannelsOperation : PushNotificationsOperation() + object PNAddPushNotificationsOnChannelsOperation : PushNotificationsOperation() + object PNRemovePushNotificationsFromChannelsOperation : PushNotificationsOperation() + object PNRemoveAllPushNotificationsOperation : PushNotificationsOperation() + + object PNAccessManagerAudit : PAMOperation() + object PNAccessManagerGrant : PAMOperation() + + object PNMessageCountOperation : MessageCountsOperation() + + object PNSignalOperation : SignalsOperation() + + object PNCreateUserOperation : ObjectsOperation() + object PNGetUserOperation : ObjectsOperation() + object PNGetUsersOperation : ObjectsOperation() + object PNUpdateUserOperation : ObjectsOperation() + object PNDeleteUserOperation : ObjectsOperation() + object PNCreateSpaceOperation : ObjectsOperation() + object PNGetSpaceOperation : ObjectsOperation() + object PNGetSpacesOperation : ObjectsOperation() + object PNUpdateSpaceOperation : ObjectsOperation() + object PNDeleteSpaceOperation : ObjectsOperation() + object PNGetMembers : ObjectsOperation() + object PNManageMembers : ObjectsOperation() + object PNGetMemberships : ObjectsOperation() + object PNManageMemberships : ObjectsOperation() + + object PNAccessManagerGrantToken : PAMV3Operation() + + object PNAddMessageAction : MessageActionsOperation() + object PNGetMessageActions : MessageActionsOperation() + object PNDeleteMessageAction : MessageActionsOperation() + + object PNTimeOperation : TimeOperation() + + override fun toString(): String { + return this.javaClass.simpleName + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt b/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt new file mode 100644 index 000000000..1a14b6a80 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNPushEnvironment.kt @@ -0,0 +1,6 @@ +package com.pubnub.api.enums + +enum class PNPushEnvironment { + DEVELOPMENT, + PRODUCTION +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt b/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt new file mode 100644 index 000000000..73429ccf3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNPushType.kt @@ -0,0 +1,17 @@ +package com.pubnub.api.enums + +import java.util.* + +enum class PNPushType(s: String) { + + APNS("apns"), + MPNS("mpns"), + FCM("gcm"), + APNS2("apns2"); + + private val value: String = s + + fun toParamString(): String { + return value.toLowerCase(Locale.US) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt b/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt new file mode 100644 index 000000000..bc0bce39a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNReconnectionPolicy.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.enums + +enum class PNReconnectionPolicy { + NONE, + LINEAR, + EXPONENTIAL +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt b/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt new file mode 100644 index 000000000..bb46a7695 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/enums/PNStatusCategory.kt @@ -0,0 +1,24 @@ +package com.pubnub.api.enums + +enum class PNStatusCategory { + PNUnknownCategory, + PNAcknowledgmentCategory, + PNAccessDeniedCategory, + PNTimeoutCategory, + PNNetworkIssuesCategory, + PNConnectedCategory, + PNReconnectedCategory, + PNDisconnectedCategory, + PNUnexpectedDisconnectCategory, + PNCancelledCategory, + PNBadRequestCategory, + PNMalformedFilterExpressionCategory, + PNMalformedResponseCategory, + PNDecryptionErrorCategory, + PNTLSConnectionFailedCategory, + PNTLSUntrustedCertificateCategory, + + PNRequestMessageCountExceededCategory, + PNReconnectionAttemptsExhausted, + PNNotFoundCategory +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt b/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt new file mode 100644 index 000000000..9ee09ba6a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/interceptor/SignatureInterceptor.kt @@ -0,0 +1,17 @@ +package com.pubnub.api.interceptor + +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubUtil +import okhttp3.Interceptor +import okhttp3.Response + +class SignatureInterceptor(val pubnub: PubNub) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val request = PubNubUtil.signRequest(originalRequest, pubnub.configuration, pubnub.timestamp()) + return chain.proceed(request) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt b/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt new file mode 100644 index 000000000..3a4640ac2 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/BasePathManager.kt @@ -0,0 +1,66 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PNConfiguration + +internal class BasePathManager(private val config: PNConfiguration) { + + /** + * for cache busting, the current subdomain number used. + */ + private var currentSubdomain = 1 + + /** + * if using cache busting, this is the max number of subdomains that are supported. + */ + private val MAX_SUBDOMAIN = 20 + + /** + * default subdomain used if cache busting is disabled. + */ + + private val DEFAULT_SUBDOMAIN = "ps" + + /** + * default base path if a custom one is not provided. + */ + + private val DEFAULT_BASE_PATH = "pndsn.com" + + fun basePath(): String { + val basePathBuilder = StringBuilder("http") + .append(if (config.secure) "s" else "") + .append("://") + + when { + config.isOriginValid() -> { + basePathBuilder.append(config.origin) + } + config.cacheBusting -> { + basePathBuilder + .append("ps") + .append(currentSubdomain) + .append(".") + .append(DEFAULT_BASE_PATH) + + incrementSubdomain() + } + else -> { + basePathBuilder + .append(DEFAULT_SUBDOMAIN) + .append(".") + .append(DEFAULT_BASE_PATH) + } + } + + return basePathBuilder.toString() + } + + private fun incrementSubdomain() { + if (currentSubdomain == MAX_SUBDOMAIN) { + currentSubdomain = 1 + } else { + currentSubdomain++ + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt b/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt new file mode 100644 index 000000000..16d1e90f3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/DuplicationManager.kt @@ -0,0 +1,26 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PNConfiguration +import com.pubnub.api.models.server.SubscribeMessage + +internal class DuplicationManager(private val config: PNConfiguration) { + + private val hashHistory: ArrayList = ArrayList() + + private fun getKey(message: SubscribeMessage) = + with(message) { + "${publishMetaData?.publishTimetoken}-${payload.hashCode()}" + } + + fun isDuplicate(message: SubscribeMessage) = hashHistory.contains(getKey(message)) + + fun addEntry(message: SubscribeMessage) { + if (hashHistory.size >= config.maximumMessagesCacheSize) { + hashHistory.removeAt(0) + } + hashHistory.add(getKey(message)) + } + + fun clearHistory() = hashHistory.clear() + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt b/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt new file mode 100644 index 000000000..8a70b7459 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/ListenerManager.kt @@ -0,0 +1,57 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import java.util.* + +internal class ListenerManager(val pubnub: PubNub) { + private val listeners = mutableListOf() + + fun addListener(listener: SubscribeCallback) { + synchronized(listeners) { + listeners.add(listener) + } + } + + fun removeListener(listener: SubscribeCallback) { + synchronized(listeners) { + listeners.remove(listener) + } + } + + private fun getListeners(): List { + val tempCallbackList = ArrayList() + synchronized(listeners) { + tempCallbackList.addAll(listeners) + } + return tempCallbackList + } + + @Synchronized + fun announce(status: PNStatus) { + getListeners().forEach { it.status(pubnub, status) } + } + + fun announce(message: PNMessageResult) { + getListeners().forEach { it.message(pubnub, message) } + } + + fun announce(presence: PNPresenceEventResult) { + getListeners().forEach { it.presence(pubnub, presence) } + } + + fun announce(signal: PNSignalResult) { + getListeners().forEach { it.signal(pubnub, signal) } + } + + fun announce(messageAction: PNMessageActionResult) { + getListeners().forEach { it.messageAction(pubnub, messageAction) } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt b/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt new file mode 100644 index 000000000..4d9c424c5 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/MapperManager.kt @@ -0,0 +1,200 @@ +package com.pubnub.api.managers + +import com.google.gson.* +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import retrofit2.Converter +import retrofit2.converter.gson.GsonConverterFactory +import java.lang.reflect.Type + +class MapperManager { + + private val objectMapper: Gson + internal val converterFactory: Converter.Factory + + init { + val booleanAsIntAdapter = object : TypeAdapter() { + override fun write(out: JsonWriter?, value: Boolean?) { + if (value == null) { + out?.nullValue() + } else { + out?.value(value) + } + } + + override fun read(_in: JsonReader): Boolean? { + val peek: JsonToken = _in.peek() + return when (peek) { + JsonToken.BOOLEAN -> _in.nextBoolean() + JsonToken.NUMBER -> _in.nextInt() != 0 + JsonToken.STRING -> java.lang.Boolean.parseBoolean(_in.nextString()) + else -> throw IllegalStateException("Expected BOOLEAN or NUMBER but was $peek") + } + } + } + + objectMapper = GsonBuilder() + .registerTypeAdapter(Boolean::class.javaObjectType, booleanAsIntAdapter) + .registerTypeAdapter(Boolean::class.javaPrimitiveType, booleanAsIntAdapter) + .registerTypeAdapter(Boolean::class.java, booleanAsIntAdapter) + .registerTypeAdapter(JSONObject::class.java, JSONObjectAdapter()) + .registerTypeAdapter(JSONArray::class.java, JSONArrayAdapter()) + .create() + converterFactory = GsonConverterFactory.create(objectMapper) + } + + fun hasField(element: JsonElement, field: String) = element.asJsonObject.has(field) + + fun getField(element: JsonElement?, field: String): JsonElement? { + if (element?.isJsonObject!!) { + return element.asJsonObject.get(field) + } + return null + } + + fun getArrayIterator(element: JsonElement?) = element?.asJsonArray?.iterator() + + fun getArrayIterator(element: JsonElement, field: String) = element.asJsonObject.get(field).asJsonArray.iterator() + + fun getObjectIterator(element: JsonElement) = element.asJsonObject.entrySet().iterator() + + fun getObjectIterator(element: JsonElement, field: String) = + element.asJsonObject.get(field).asJsonObject.entrySet().iterator() + + fun elementToString(element: JsonElement?) = element?.asString + + fun elementToString(element: JsonElement?, field: String) = element?.asJsonObject?.get(field)?.asString + + fun elementToInt(element: JsonElement, field: String) = element.asJsonObject.get(field).asInt + + fun isJsonObject(element: JsonElement) = element.isJsonObject + + fun getAsObject(element: JsonElement) = element.asJsonObject + + fun getAsBoolean(element: JsonElement, field: String) = element.asJsonObject.get(field)?.asBoolean + .run { this != null } + + fun putOnObject(element: JsonObject, key: String, value: JsonElement) = element.add(key, value) + + fun getArrayElement(element: JsonElement, index: Int) = element.asJsonArray.get(index) + + fun elementToLong(element: JsonElement) = element.asLong + + fun elementToLong(element: JsonElement, field: String) = element.asJsonObject.get(field).asLong + + fun getAsArray(element: JsonElement) = element.asJsonArray + + fun fromJson(input: String?, clazz: Class): T { + return try { + this.objectMapper.fromJson(input, clazz) + } catch (e: JsonParseException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.message + } + } + } + + fun fromJson(input: String?, typeOfT: Type): T { + return try { + this.objectMapper.fromJson(input, typeOfT) + } catch (e: JsonParseException) { + throw PubNubException(PubNubError.PARSING_ERROR).apply { + errorMessage = e.message + } + } + } + + fun convertValue(input: JsonElement?, clazz: Class): T { + return this.objectMapper.fromJson(input, clazz) as T + } + + fun convertValue(o: Any?, clazz: Class?): T { + return this.objectMapper.fromJson(toJson(o), clazz) as T + } + + fun toJson(input: Any?): String { + return try { + this.objectMapper.toJson(input) + } catch (e: JsonParseException) { + throw PubNubException(PubNubError.JSON_ERROR).apply { + errorMessage = e.message + } + } + } + + private class JSONObjectAdapter : JsonSerializer, JsonDeserializer { + override fun serialize( + src: JSONObject?, + typeOfSrc: Type?, + context: JsonSerializationContext + ): JsonElement? { + if (src == null) { + return null + } + val jsonObject = JsonObject() + val keys: Iterator = src.keys() + while (keys.hasNext()) { + val key = keys.next() + val value: Any = src.opt(key) + val jsonElement = context.serialize(value, value.javaClass) + jsonObject.add(key, jsonElement) + } + return jsonObject + } + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): JSONObject? { + return if (json == null) { + null + } else try { + JSONObject(json.toString()) + } catch (e: JSONException) { + e.printStackTrace() + throw JsonParseException(e) + } + } + } + + private class JSONArrayAdapter : JsonSerializer, JsonDeserializer { + override fun serialize( + src: JSONArray?, + typeOfSrc: Type?, + context: JsonSerializationContext + ): JsonElement? { + if (src == null) { + return null + } + val jsonArray = JsonArray() + for (i in 0 until src.length()) { + val `object`: Any = src.opt(i) + val jsonElement = context.serialize(`object`, `object`.javaClass) + jsonArray.add(jsonElement) + } + return jsonArray + } + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): JSONArray? { + return if (json == null) { + null + } else try { + JSONArray(json.toString()) + } catch (e: JSONException) { + e.printStackTrace() + throw JsonParseException(e) + } + } + } +} diff --git a/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt b/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt new file mode 100644 index 000000000..589d0473b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/PublishSequenceManager.kt @@ -0,0 +1,17 @@ +package com.pubnub.api.managers + +internal class PublishSequenceManager(private val maxSequence: Int) { + + private var nextSequence = 0 + + internal fun nextSequence(): Int { + synchronized(nextSequence) { + if (maxSequence == nextSequence) { + nextSequence = 1 + } else { + nextSequence++ + } + return nextSequence + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt b/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt new file mode 100644 index 000000000..2b71e118d --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/ReconnectionManager.kt @@ -0,0 +1,111 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PNConfiguration +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.ReconnectionCallback +import com.pubnub.api.enums.PNReconnectionPolicy +import org.slf4j.LoggerFactory +import java.util.* +import kotlin.math.pow + +internal class ReconnectionManager(val pubnub: PubNub) { + + private val log = LoggerFactory.getLogger("ReconnectionManager") + + private companion object { + private const val LINEAR_INTERVAL = 3 + private const val MIN_EXPONENTIAL_BACKOFF = 1 + private const val MAX_EXPONENTIAL_BACKOFF = 32 + + private const val MILLISECONDS = 1000 + } + + internal lateinit var reconnectionCallback: ReconnectionCallback + + private var exponentialMultiplier = 1 + private var failedCalls = 0 + + private lateinit var pnReconnectionPolicy: PNReconnectionPolicy + private var maxConnectionRetries = -1 + + private val timer = Timer() + + internal fun startPolling(pnConfiguration: PNConfiguration) { + pnReconnectionPolicy = pnConfiguration.reconnectionPolicy + maxConnectionRetries = pnConfiguration.maximumReconnectionRetries + + if (isReconnectionPolicyUndefined()) + return + + exponentialMultiplier = 1 + failedCalls = 0 + + registerHeartbeatTimer() + } + + private fun registerHeartbeatTimer() { + // make sure only one timer is running at a time. + stopHeartbeatTimer() + + if (isReconnectionPolicyUndefined()) { + return + } + + if (maxConnectionRetries != -1 && failedCalls >= maxConnectionRetries) { + reconnectionCallback.onMaxReconnectionExhaustion() + return + } + timer.schedule(object : TimerTask() { + override fun run() { + callTime() + } + }, getBestInterval() * MILLISECONDS.toLong()) + } + + private fun getBestInterval(): Int { + var timerInterval = LINEAR_INTERVAL + if (pnReconnectionPolicy == PNReconnectionPolicy.EXPONENTIAL) { + timerInterval = (2.0.pow(exponentialMultiplier.toDouble()) - 1).toInt() + if (timerInterval > MAX_EXPONENTIAL_BACKOFF) { + timerInterval = MIN_EXPONENTIAL_BACKOFF + exponentialMultiplier = 1 + log.info("timerInterval > MAXEXPONENTIALBACKOFF at: " + Calendar.getInstance().time.toString()) + } else if (timerInterval < 1) { + timerInterval = MIN_EXPONENTIAL_BACKOFF + } + log.info("timerInterval = " + timerInterval + " at: " + Calendar.getInstance().time.toString()) + } + if (pnReconnectionPolicy == PNReconnectionPolicy.LINEAR) { + timerInterval = LINEAR_INTERVAL + } + return timerInterval + } + + private fun stopHeartbeatTimer() { + timer.cancel() + } + + private fun callTime() { + pubnub.time() + .async { _, status -> + if (!status.error) { + stopHeartbeatTimer() + reconnectionCallback.onReconnection() + } else { + log.info("callTime at ${System.currentTimeMillis()}") + exponentialMultiplier++ + failedCalls++ + registerHeartbeatTimer() + } + } + } + + private fun isReconnectionPolicyUndefined(): Boolean { + if (pnReconnectionPolicy == PNReconnectionPolicy.NONE) { + log.info("reconnection policy is disabled, please handle reconnection manually.") + return true + } + return false + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt b/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt new file mode 100644 index 000000000..04ec1042b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/RetrofitManager.kt @@ -0,0 +1,113 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNLogVerbosity +import com.pubnub.api.interceptor.SignatureInterceptor +import com.pubnub.api.services.* +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit + +internal class RetrofitManager(val pubnub: PubNub) { + + private val transactionClientInstance: OkHttpClient by lazy { + createOkHttpClient(pubnub.configuration.nonSubscribeRequestTimeout) + } + + private val subscriptionClientInstance: OkHttpClient by lazy { + createOkHttpClient(pubnub.configuration.subscribeTimeout) + } + + private val signatureInterceptor: SignatureInterceptor + + internal val timeService: TimeService + internal val publishService: PublishService + internal val historyService: HistoryService + internal val presenceService: PresenceService + internal val messageActionService: MessageActionService + internal val signalService: SignalService + internal val channelGroupService: ChannelGroupService + internal val pushService: PushService + internal val accessManagerService: AccessManagerService + + internal val subscribeService: SubscribeService + + + init { + signatureInterceptor = SignatureInterceptor(pubnub) + + val transactionInstance = createRetrofit(transactionClientInstance) + val subscriptionInstance = createRetrofit(subscriptionClientInstance) + + timeService = transactionInstance.create(TimeService::class.java) + publishService = transactionInstance.create(PublishService::class.java) + historyService = transactionInstance.create(HistoryService::class.java) + presenceService = transactionInstance.create(PresenceService::class.java) + messageActionService = transactionInstance.create(MessageActionService::class.java) + signalService = transactionInstance.create(SignalService::class.java) + channelGroupService = transactionInstance.create(ChannelGroupService::class.java) + pushService = transactionInstance.create(PushService::class.java) + accessManagerService = transactionInstance.create(AccessManagerService::class.java) + + subscribeService = subscriptionInstance.create(SubscribeService::class.java) + } + + private fun createOkHttpClient(readTimeout: Int): OkHttpClient { + val okHttpBuilder = OkHttpClient.Builder() + .retryOnConnectionFailure(true) + .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) + .connectTimeout(pubnub.configuration.connectTimeout.toLong(), TimeUnit.SECONDS) + + with(pubnub.configuration) { + if (logVerbosity == PNLogVerbosity.BODY) { + okHttpBuilder.addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + } + if (sslSocketFactory != null && x509ExtendedTrustManager != null) { + okHttpBuilder.sslSocketFactory( + pubnub.configuration.sslSocketFactory!!, + pubnub.configuration.x509ExtendedTrustManager!! + ) + } + connectionSpec?.let { okHttpBuilder.connectionSpecs(listOf(it)) } + hostnameVerifier?.let { okHttpBuilder.hostnameVerifier(it) } + proxy?.let { okHttpBuilder.proxy(it) } + proxySelector?.let { okHttpBuilder.proxySelector(it) } + proxyAuthenticator?.let { okHttpBuilder.proxyAuthenticator(it) } + certificatePinner?.let { okHttpBuilder.certificatePinner(it) } + } + + okHttpBuilder.addInterceptor(signatureInterceptor) + + val okHttpClient = okHttpBuilder.build() + + pubnub.configuration.maximumConnections?.let { okHttpClient.dispatcher().maxRequestsPerHost = it } + + return okHttpClient + } + + private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { + val retrofitBuilder = Retrofit.Builder() + .client(okHttpClient) + .baseUrl(pubnub.baseUrl()) + .addConverterFactory(pubnub.mapper.converterFactory) + + return retrofitBuilder.build() + } + + fun destroy(force: Boolean = false) { + closeExecutor(transactionClientInstance, force) + closeExecutor(subscriptionClientInstance, force) + } + + private fun closeExecutor(client: OkHttpClient, force: Boolean) { + client.dispatcher().cancelAll() + if (force) { + client.connectionPool().evictAll() + val executorService = client.dispatcher().executorService() + executorService.shutdown() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/StateManager.kt b/src/main/kotlin/com/pubnub/api/managers/StateManager.kt new file mode 100644 index 000000000..861966ca8 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/StateManager.kt @@ -0,0 +1,170 @@ +package com.pubnub.api.managers + +import com.pubnub.api.builder.PresenceOperation +import com.pubnub.api.builder.StateOperation +import com.pubnub.api.builder.SubscribeOperation +import com.pubnub.api.builder.UnsubscribeOperation +import com.pubnub.api.models.SubscriptionItem + +internal class StateManager { + + /** + * Contains a list of subscribed channels + */ + private val channels: HashMap = hashMapOf() + + /** + * Contains a list of subscribed presence channels. + */ + private val presenceChannels: HashMap = hashMapOf() + + /** + * Contains a list of subscribed channel groups. + */ + private val groups: HashMap = hashMapOf() + + /** + * Contains a list of subscribed presence channel groups. + */ + private val presenceGroups: HashMap = hashMapOf() + + private val heartbeatChannels: HashMap = hashMapOf() + private val heartbeatGroups: HashMap = hashMapOf() + + @Synchronized + internal fun adaptSubscribeBuilder(subscribeOperation: SubscribeOperation) { + for (channel in subscribeOperation.channels) { + if (channel.isEmpty()) { + continue + } + val subscriptionItem = SubscriptionItem(channel) + + channels[channel] = subscriptionItem + + if (subscribeOperation.presenceEnabled) { + val presenceSubscriptionItem = SubscriptionItem(channel) + presenceChannels[channel] = presenceSubscriptionItem + } + } + for (channelGroup in subscribeOperation.channelGroups) { + if (channelGroup.isEmpty()) { + continue + } + val subscriptionItem = SubscriptionItem(channelGroup) + groups.put(channelGroup, subscriptionItem) + if (subscribeOperation.presenceEnabled) { + val presenceSubscriptionItem = SubscriptionItem(channelGroup) + presenceGroups[channelGroup] = presenceSubscriptionItem + } + } + } + + @Synchronized + internal fun adaptStateBuilder(stateOperation: StateOperation) { + for (channel in stateOperation.channels) { + val subscribedChannel = channels[channel] + subscribedChannel?.state = stateOperation.state + } + for (channelGroup in stateOperation.channelGroups) { + val subscribedChannelGroup = groups[channelGroup] + subscribedChannelGroup?.state = stateOperation.state + } + } + + @Synchronized + internal fun adaptUnsubscribeBuilder(unsubscribeOperation: UnsubscribeOperation) { + for (channel in unsubscribeOperation.channels) { + channels.remove(channel) + presenceChannels.remove(channel) + } + for (channelGroup in unsubscribeOperation.channelGroups) { + groups.remove(channelGroup) + presenceGroups.remove(channelGroup) + } + } + + @Synchronized + internal fun adaptPresenceBuilder(presenceOperation: PresenceOperation) { + for (channel in presenceOperation.channels) { + if (channel.isEmpty()) { + continue + } + if (presenceOperation.connected) { + val subscriptionItem = SubscriptionItem(channel) + heartbeatChannels[channel] = subscriptionItem + } else { + heartbeatChannels.remove(channel) + } + } + for (channelGroup in presenceOperation.channelGroups) { + if (channelGroup.isEmpty()) { + continue + } + if (presenceOperation.connected) { + val subscriptionItem = SubscriptionItem(channelGroup) + heartbeatGroups[channelGroup] = subscriptionItem + } else { + heartbeatGroups.remove(channelGroup) + } + } + } + + @Synchronized + fun createStatePayload(): Map { + val stateResponse: HashMap = hashMapOf() + for (channel in channels.values) { + if (channel.state != null) { + stateResponse[channel.name] = channel.state + } + } + for (channelGroup in groups.values) { + if (channelGroup.state != null) { + stateResponse[channelGroup.name] = channelGroup.state + } + } + return stateResponse + } + + @Synchronized + fun prepareChannelList(includePresence: Boolean): List { + return prepareMembershipList(channels, presenceChannels, includePresence) + } + + @Synchronized + fun prepareChannelGroupList(includePresence: Boolean): List { + return prepareMembershipList(groups, presenceGroups, includePresence) + } + + @Synchronized + fun prepareHeartbeatChannelList(includePresence: Boolean): List { + return prepareMembershipList(heartbeatChannels, presenceChannels, includePresence) + } + + @Synchronized + fun prepareHeartbeatChannelGroupList(includePresence: Boolean): List { + return prepareMembershipList(heartbeatGroups, presenceGroups, includePresence) + } + + @Synchronized + fun isEmpty(): Boolean { + return channels.isEmpty() && presenceChannels.isEmpty() && groups.isEmpty() && presenceGroups.isEmpty() + } + + @Synchronized + private fun prepareMembershipList( + dataStorage: Map, + presenceStorage: Map, + includePresence: Boolean + ): List { + val response: MutableList = ArrayList() + for (channelGroupItem in dataStorage.values) { + response.add(channelGroupItem.name) + } + if (includePresence) { + for (presenceChannelGroupItem in presenceStorage.values) { + response.add(presenceChannelGroupItem.name + "-pnpres") + } + } + return response + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt b/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt new file mode 100644 index 000000000..9a646c270 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/SubscriptionManager.kt @@ -0,0 +1,394 @@ +package com.pubnub.api.managers + +import com.pubnub.api.PubNub +import com.pubnub.api.builder.PresenceOperation +import com.pubnub.api.builder.StateOperation +import com.pubnub.api.builder.SubscribeOperation +import com.pubnub.api.builder.UnsubscribeOperation +import com.pubnub.api.callbacks.ReconnectionCallback +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.endpoints.presence.Heartbeat +import com.pubnub.api.endpoints.presence.Leave +import com.pubnub.api.endpoints.pubsub.Subscribe +import com.pubnub.api.enums.PNHeartbeatNotificationOptions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.server.SubscribeMessage +import com.pubnub.api.workers.SubscribeMessageWorker +import java.util.* +import java.util.concurrent.LinkedBlockingQueue +import kotlin.concurrent.timerTask + +class SubscriptionManager(val pubnub: PubNub) { + + private companion object { + private const val HEARTBEAT_INTERVAL_MULTIPLIER = 1000L + } + + private var subscribeCall: Subscribe? = null + private var heartbeatCall: Heartbeat? = null + + private var messageQueue: LinkedBlockingQueue = LinkedBlockingQueue() + + private var duplicationManager: DuplicationManager = DuplicationManager(pubnub.configuration) + + /** + * Store the latest timetoken to subscribe with, null by default to get the latest timetoken. + */ + private var timetoken = 0L + private var storedTimetoken: Long? = null // when changing the channel mix, store the timetoken for a later date. + + /** + * Keep track of Region to support PSV2 specification. + */ + private var region: String? = null + + /** + * Timer for heartbeat operations. + */ + private var heartbeatTimer: Timer? = null + + private val subscriptionState = StateManager() + internal val listenerManager = ListenerManager(pubnub) + private val reconnectionManager = ReconnectionManager(pubnub) + + private var consumerThread: Thread? = null + + /** + * lever to indicate if an announcement to the user about the subscription should be made. + * the announcement happens only after the channel mix has been changed. + */ + private var subscriptionStatusAnnounced: Boolean = false + + init { + + reconnectionManager.reconnectionCallback = object : ReconnectionCallback() { + override fun onReconnection() { + reconnect() + listenerManager.announce( + PNStatus( + category = PNStatusCategory.PNReconnectedCategory, + operation = PNOperationType.PNSubscribeOperation, + error = false, + affectedChannels = subscriptionState.prepareChannelList(true), + affectedChannelGroups = subscriptionState.prepareChannelGroupList(true) + ) + ) + subscriptionStatusAnnounced = true + } + + override fun onMaxReconnectionExhaustion() { + listenerManager.announce( + PNStatus( + category = PNStatusCategory.PNReconnectionAttemptsExhausted, + operation = PNOperationType.PNSubscribeOperation, + error = false, + affectedChannels = subscriptionState.prepareChannelList(true), + affectedChannelGroups = subscriptionState.prepareChannelGroupList(true) + ) + ) + + disconnect() + } + } + + if (pubnub.configuration.startSubscriberThread) { + consumerThread = Thread( + SubscribeMessageWorker( + pubnub, + listenerManager, + messageQueue, + duplicationManager + ) + ) + consumerThread?.name = "Subscription Manager Consumer Thread" + consumerThread?.start() + } + } + + @Synchronized + fun getSubscribedChannels(): List { + return subscriptionState.prepareChannelList(false) + } + + @Synchronized + fun getSubscribedChannelGroups(): List { + return subscriptionState.prepareChannelGroupList(false) + } + + @Synchronized + internal fun adaptStateBuilder(stateOperation: StateOperation?) { + subscriptionState.adaptStateBuilder(stateOperation!!) + reconnect() + } + + @Synchronized + internal fun adaptSubscribeBuilder(subscribeOperation: SubscribeOperation) { + subscriptionState.adaptSubscribeBuilder(subscribeOperation) + + // the channel mix changed, on the successful subscribe, there is going to be announcement. + subscriptionStatusAnnounced = false + duplicationManager.clearHistory() + + timetoken = subscribeOperation.timetoken + + // if the timetoken is not at starting position, reset the timetoken to get a connected event + // and store the old timetoken to be reused later during subscribe. + if (timetoken != 0L) { + storedTimetoken = timetoken + } + timetoken = 0L + reconnect() + } + + @Synchronized + fun reconnect() { + startSubscribeLoop() + registerHeartbeatTimer() + } + + @Synchronized + fun disconnect() { + heartbeatTimer?.cancel() + stopSubscribeLoop() + } + + private fun registerHeartbeatTimer() { + // make sure only one timer is running at a time. + heartbeatTimer?.cancel() + + // if the interval is 0 or less, do not start the timer + if (pubnub.configuration.heartbeatInterval <= 0) { + return + } + + heartbeatTimer = Timer() + heartbeatTimer?.schedule( + timerTask { + performHeartbeatLoop() + }, + 0, + pubnub.configuration.heartbeatInterval * HEARTBEAT_INTERVAL_MULTIPLIER + ) + } + + private fun performHeartbeatLoop() { + heartbeatCall?.silentCancel() + val presenceChannels = subscriptionState.prepareChannelList(false) + val presenceChannelGroups = + subscriptionState.prepareChannelGroupList(false) + val heartbeatChannels = + subscriptionState.prepareHeartbeatChannelList(false) + val heartbeatChannelGroups = + subscriptionState.prepareHeartbeatChannelGroupList(false) + // do not start the loop if we do not have any presence channels or channel groups enabled. + if (presenceChannels.isEmpty() + && presenceChannelGroups.isEmpty() + && heartbeatChannels.isEmpty() + && heartbeatChannelGroups.isEmpty() + ) { + return + } + val channels: MutableList = ArrayList() + channels.addAll(presenceChannels) + channels.addAll(heartbeatChannels) + val groups: MutableList = ArrayList() + groups.addAll(presenceChannelGroups) + groups.addAll(heartbeatChannelGroups) + heartbeatCall = Heartbeat(pubnub, channels, groups) + heartbeatCall?.async { _, status -> + val heartbeatVerbosity = pubnub.configuration.heartbeatNotificationOptions + + if (status.error) { + if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL + || heartbeatVerbosity == PNHeartbeatNotificationOptions.FAILURES + ) { + listenerManager.announce(status) + } + // stop the heartbeating logic since an error happened. + heartbeatTimer?.cancel() + } else { + if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL) { + listenerManager.announce(status) + } + } + } + } + + private fun startSubscribeLoop() { + // this function can be called from different points, make sure any old loop is closed + stopSubscribeLoop() + + val combinedChannels = subscriptionState.prepareChannelList(true) + val combinedChannelGroups = subscriptionState.prepareChannelGroupList(true) + val stateStorage = subscriptionState.createStatePayload() + + // do not start the subscribe loop if we have no channels to subscribe to. + if (combinedChannels.isEmpty() && combinedChannelGroups.isEmpty()) { + return + } + + subscribeCall = Subscribe(pubnub).apply { + channels = combinedChannels + channelGroups = combinedChannelGroups + timetoken = this@SubscriptionManager.timetoken + region = this@SubscriptionManager.region + pubnub.configuration.isFilterExpressionKeyValid { + filterExpression = this + } + state = stateStorage + } + + subscribeCall?.async { result, status -> + if (status.error) { + if (status.category == PNStatusCategory.PNTimeoutCategory) { + startSubscribeLoop() + return@async + } + + disconnect() + listenerManager.announce(status) + + if (status.category == PNStatusCategory.PNUnexpectedDisconnectCategory) { + // stop all announcements and ask the reconnection manager to start polling for connection restoration.. + reconnectionManager.startPolling(pubnub.configuration) + } + + return@async + } + + if (!subscriptionStatusAnnounced) { + val pnStatus = createPublicStatus(status).apply { + category = PNStatusCategory.PNConnectedCategory + error = false + } + subscriptionStatusAnnounced = true + listenerManager.announce(pnStatus) + } + + pubnub.configuration.requestMessageCountThreshold?.let { + // todo default value of size if all ?s are null + if (it <= result!!.messages.size) { + listenerManager.announce( + createPublicStatus(status).apply { + category = PNStatusCategory.PNRequestMessageCountExceededCategory + error = false + } + ) + } + } + + if (result!!.messages.isNotEmpty()) { + messageQueue.addAll(result.messages) + } + + if (storedTimetoken != null) { + timetoken = storedTimetoken!! + storedTimetoken = null + } else { + timetoken = result.metadata.timetoken + } + + region = result.metadata.region + startSubscribeLoop() + + } + + } + + private fun stopSubscribeLoop() { + subscribeCall?.silentCancel() + } + + private fun stopHeartbeatLoop() { + heartbeatCall?.silentCancel() + } + + internal fun adaptPresenceBuilder(presenceOperation: PresenceOperation) { + subscriptionState.adaptPresenceBuilder(presenceOperation) + + if (!pubnub.configuration.suppressLeaveEvents && !presenceOperation.connected) { + Leave(pubnub).apply { + channels = presenceOperation.channels + channelGroups = presenceOperation.channelGroups + }.async { _, status -> + listenerManager.announce(status) + } + } + + registerHeartbeatTimer() + } + + @Synchronized + internal fun adaptUnsubscribeBuilder(unsubscribeOperation: UnsubscribeOperation) { + subscriptionState.adaptUnsubscribeBuilder(unsubscribeOperation) + subscriptionStatusAnnounced = false + + if (!pubnub.configuration.suppressLeaveEvents) { + Leave(pubnub).apply { + channels = unsubscribeOperation.channels + channelGroups = unsubscribeOperation.channelGroups + }.async { _, status -> + listenerManager.announce(status) + } + } + + // if we unsubscribed from all the channels, reset the timetoken back to zero and remove the region. + if (subscriptionState.isEmpty()) { + region = null + storedTimetoken = null + timetoken = 0L + } else { + storedTimetoken = timetoken + timetoken = 0L + } + + reconnect() + } + + @Synchronized + fun unsubscribeAll() { + adaptUnsubscribeBuilder( + UnsubscribeOperation().apply { + channels = subscriptionState.prepareChannelList(false) + channelGroups = subscriptionState.prepareChannelGroupList(false) + } + ) + } + + private fun createPublicStatus(privateStatus: PNStatus): PNStatus { + with(privateStatus) { + return PNStatus( + category = category, + error = this.error, + operation = operation, + exception = exception, + statusCode = statusCode, + tlsEnabled = tlsEnabled, + origin = origin, + uuid = uuid, + authKey = authKey, + affectedChannels = affectedChannels, + affectedChannelGroups = affectedChannelGroups + ) + } + } + + fun addListener(listener: SubscribeCallback) { + listenerManager.addListener(listener) + } + + fun removeListener(listener: SubscribeCallback) { + listenerManager.removeListener(listener) + } + + @Synchronized + fun destroy(forceDestroy: Boolean = false) { + disconnect() + if (forceDestroy && consumerThread != null) { + consumerThread!!.interrupt() + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt b/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt new file mode 100644 index 000000000..f959c1ac9 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/managers/TelemetryManager.kt @@ -0,0 +1,120 @@ +package com.pubnub.api.managers + +import com.pubnub.api.enums.PNOperationType +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.set + +internal class TelemetryManager { + + companion object { + private const val MAX_FRACTION_DIGITS = 3 + private const val TIMESTAMP_DIVIDER = 1000 + private const val MAXIMUM_LATENCY_DATA_AGE = 60.0 + private const val CLEAN_UP_INTERVAL = 1 + private const val CLEAN_UP_INTERVAL_MULTIPLIER = 1000 + } + + private var timer: Timer? = Timer() + private val latencies: HashMap>> = HashMap() + private val numberFormat by lazy { + NumberFormat.getNumberInstance(Locale.US).apply { + maximumFractionDigits = MAX_FRACTION_DIGITS + roundingMode = RoundingMode.HALF_UP + isGroupingUsed = false + } + } + + init { + startCleanUpTimer() + } + + @Synchronized + fun operationsLatency(): Map { + val operationLatencies = HashMap() + latencies.entries.forEach { + val latencyKey = "l_${it.key}" + val endpointAverageLatency = averageLatencyFromData(it.value) + if (endpointAverageLatency > 0.0f) { + operationLatencies[latencyKey] = numberFormat.format(endpointAverageLatency) + } + } + return operationLatencies + } + + private fun startCleanUpTimer() { + val interval = (CLEAN_UP_INTERVAL * CLEAN_UP_INTERVAL_MULTIPLIER).toLong() + + stopCleanUpTimer() + timer = Timer() + timer?.schedule(object : TimerTask() { + override fun run() { + cleanUpTelemetryData() + } + }, interval, interval) + } + + + internal fun stopCleanUpTimer() { + this.timer?.cancel() + } + + @Synchronized + private fun cleanUpTelemetryData() { + val currentDate = Date().time / (TIMESTAMP_DIVIDER.toDouble()) + val endpoints = latencies.keys.toList() + endpoints.forEach { + val outdatedLatencies = ArrayList>() + val operationLatencies = ArrayList>() + operationLatencies.forEach { map: Map -> + map["d"]?.let { d: Double -> + if (currentDate - d > MAXIMUM_LATENCY_DATA_AGE) { + outdatedLatencies.add(map) + } + } + } + if (outdatedLatencies.size > 0) { + operationLatencies.removeAll(outdatedLatencies) + } + if (operationLatencies.size == 0) { + this.latencies.remove(it) + } + } + } + + private fun averageLatencyFromData(endpointLatencies: List>): Double { + var totalLatency = 0.0 + endpointLatencies.forEach { + it["l"]?.let { l: Double -> + totalLatency += l + } + } + return totalLatency / endpointLatencies.size + } + + @Synchronized + internal fun storeLatency(latency: Long, type: PNOperationType) { + type.queryParam?.let { queryParam: String -> + if (latency > 0) { + val storeDate = Date().time / (TIMESTAMP_DIVIDER.toDouble()) + + if (latencies[queryParam] == null) { + latencies[queryParam] = ArrayList() + } + + latencies[queryParam]?.let { + latencies[queryParam] = it + + val latencyEntry = java.util.HashMap() + latencyEntry["d"] = storeDate + latencyEntry["l"] = latency.toDouble() / TIMESTAMP_DIVIDER + it.add(latencyEntry) + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/SubscriptionItem.kt b/src/main/kotlin/com/pubnub/api/models/SubscriptionItem.kt new file mode 100644 index 000000000..f59086069 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/SubscriptionItem.kt @@ -0,0 +1,6 @@ +package com.pubnub.api.models + +internal class SubscriptionItem( + internal var name: String, + internal var state: Any? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/PNPublishResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/PNPublishResult.kt new file mode 100644 index 000000000..d27b21efd --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/PNPublishResult.kt @@ -0,0 +1,5 @@ +package com.pubnub.api.models.consumer + +class PNPublishResult internal constructor( + val timetoken: Long +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/PNStatus.kt b/src/main/kotlin/com/pubnub/api/models/consumer/PNStatus.kt new file mode 100644 index 000000000..6613a4b84 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/PNStatus.kt @@ -0,0 +1,34 @@ +package com.pubnub.api.models.consumer + +import com.pubnub.api.Endpoint +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import okhttp3.Request + +data class PNStatus( + var category: PNStatusCategory, + var error: Boolean, + val operation: PNOperationType, + + val exception: PubNubException? = null, + + var statusCode: Int? = null, + var tlsEnabled: Boolean? = null, + var origin: String? = null, + var uuid: String? = null, + var authKey: String? = null, + + var affectedChannels: List = emptyList(), + var affectedChannelGroups: List = emptyList() + +) { + + var executedEndpoint: Endpoint<*, *>? = null + var clientRequest: Request? = null + + fun retry() { + executedEndpoint?.retry() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/PNTimeResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/PNTimeResult.kt new file mode 100644 index 000000000..ba93a2398 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/PNTimeResult.kt @@ -0,0 +1,5 @@ +package com.pubnub.api.models.consumer + +class PNTimeResult internal constructor( + val timetoken: Long +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/access_manager/PNAccessManagerGrantResults.kt b/src/main/kotlin/com/pubnub/api/models/consumer/access_manager/PNAccessManagerGrantResults.kt new file mode 100644 index 000000000..fa2ffabed --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/access_manager/PNAccessManagerGrantResults.kt @@ -0,0 +1,31 @@ +package com.pubnub.api.models.consumer.access_manager + +import com.google.gson.annotations.SerializedName + +class PNAccessManagerGrantResult( + val level: String, + val ttl: Int, + val subscribeKey: String, + val channels: Map?>, + val channelGroups: Map?> +) + +class PNAccessManagerKeyData { + + @SerializedName("r") + internal var readEnabled: Boolean = false + + @SerializedName("w") + internal var writeEnabled: Boolean = false + + @SerializedName("m") + internal var manageEnabled: Boolean = false + + @SerializedName("d") + internal var deleteEnabled: Boolean = false +} + +class PNAccessManagerKeysData { + @SerializedName("auths") + internal val authKeys: Map? = null +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/channel_group/PNChannelGroupsResults.kt b/src/main/kotlin/com/pubnub/api/models/consumer/channel_group/PNChannelGroupsResults.kt new file mode 100644 index 000000000..52667ba07 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/channel_group/PNChannelGroupsResults.kt @@ -0,0 +1,15 @@ +package com.pubnub.api.models.consumer.channel_group + +class PNChannelGroupsAddChannelResult + +class PNChannelGroupsDeleteGroupResult + +class PNChannelGroupsRemoveChannelResult + +class PNChannelGroupsAllChannelsResult internal constructor( + val channels: List +) + +class PNChannelGroupsListAllResult internal constructor( + val groups: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/history/PNDeleteMessagesResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNDeleteMessagesResult.kt new file mode 100644 index 000000000..886e29633 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNDeleteMessagesResult.kt @@ -0,0 +1,3 @@ +package com.pubnub.api.models.consumer.history + +class PNDeleteMessagesResult \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/history/PNFetchMessage.kt b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNFetchMessage.kt new file mode 100644 index 000000000..85bf2716e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNFetchMessage.kt @@ -0,0 +1,21 @@ +package com.pubnub.api.models.consumer.history + +import com.google.gson.JsonElement + +class PNFetchMessagesResult( + val channels: HashMap> +) + +class PNFetchMessageItem( + val message: JsonElement, + val meta: JsonElement?, + val timetoken: Long +) { + var actions: Map>>? = null + internal set +} + +class Action( + val uuid: String, + val actionTimetoken: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryItemResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryItemResult.kt new file mode 100644 index 000000000..048c3214e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryItemResult.kt @@ -0,0 +1,9 @@ +package com.pubnub.api.models.consumer.history + +import com.google.gson.JsonElement + +class PNHistoryItemResult( + val entry: JsonElement, + val timetoken: Long? = null, + val meta: JsonElement? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryResult.kt new file mode 100644 index 000000000..1811e4dab --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNHistoryResult.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.models.consumer.history + +class PNHistoryResult internal constructor( + val messages: List, + val startTimetoken: Long, + val endTimetoken: Long +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/history/PNMessageCountResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNMessageCountResult.kt new file mode 100644 index 000000000..606ec210a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/history/PNMessageCountResult.kt @@ -0,0 +1,5 @@ +package com.pubnub.api.models.consumer.history + +class PNMessageCountResult( + val channels: Map +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNAddMessageActionResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNAddMessageActionResult.kt new file mode 100644 index 000000000..4d22e88d7 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNAddMessageActionResult.kt @@ -0,0 +1,3 @@ +package com.pubnub.api.models.consumer.message_actions + +class PNAddMessageActionResult(pnMessageAction: PNMessageAction) : PNMessageAction(pnMessageAction) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNGetMessageActionsResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNGetMessageActionsResult.kt new file mode 100644 index 000000000..fed3e03fd --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNGetMessageActionsResult.kt @@ -0,0 +1,5 @@ +package com.pubnub.api.models.consumer.message_actions + +class PNGetMessageActionsResult internal constructor( + val actions: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNMessageAction.kt b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNMessageAction.kt new file mode 100644 index 000000000..80d20496b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNMessageAction.kt @@ -0,0 +1,22 @@ +package com.pubnub.api.models.consumer.message_actions + +open class PNMessageAction( + val type: String, + val value: String, + val messageTimetoken: Long +) { + + internal constructor(pnMessageAction: PNMessageAction) : this( + pnMessageAction.type, + pnMessageAction.value, + pnMessageAction.messageTimetoken + ) { + this.uuid = pnMessageAction.uuid + this.actionTimetoken = pnMessageAction.actionTimetoken + } + + var uuid: String? = null + private set + var actionTimetoken: Long? = null + private set +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNRemoveMessageActionResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNRemoveMessageActionResult.kt new file mode 100644 index 000000000..e8f88223a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/message_actions/PNRemoveMessageActionResult.kt @@ -0,0 +1,4 @@ +package com.pubnub.api.models.consumer.message_actions + +class PNRemoveMessageActionResult { +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNGetStateResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNGetStateResult.kt new file mode 100644 index 000000000..f1f213a8f --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNGetStateResult.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.models.consumer.presence + +import com.google.gson.JsonElement + +class PNGetStateResult( + val stateByUUID: Map +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNHereNow.kt b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNHereNow.kt new file mode 100644 index 000000000..5019479b6 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNHereNow.kt @@ -0,0 +1,20 @@ +package com.pubnub.api.models.consumer.presence + +import com.google.gson.JsonElement + +class PNHereNowResult( + val totalChannels: Int, + val totalOccupancy: Int, + val channels: HashMap = hashMapOf() +) + +class PNHereNowChannelData( + val channelName: String, + val occupancy: Int, + var occupants: List = emptyList() +) + +class PNHereNowOccupantData( + val uuid: String, + val state: JsonElement? = null +) diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNSetStateResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNSetStateResult.kt new file mode 100644 index 000000000..aeb37d02b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNSetStateResult.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.models.consumer.presence + +import com.google.gson.JsonElement + +class PNSetStateResult( + val state: JsonElement +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNWhereNow.kt b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNWhereNow.kt new file mode 100644 index 000000000..6f2e7de01 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/presence/PNWhereNow.kt @@ -0,0 +1,5 @@ +package com.pubnub.api.models.consumer.presence + +class PNWhereNowResult( + val channels: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/BasePubSubResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/BasePubSubResult.kt new file mode 100644 index 000000000..f0b75df87 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/BasePubSubResult.kt @@ -0,0 +1,21 @@ +package com.pubnub.api.models.consumer.pubsub + +import com.google.gson.JsonElement + +open class BasePubSubResult( + val channel: String, + val subscription: String?, + val timetoken: Long?, + val userMetadata: JsonElement?, + val publisher: String? + +) { + internal constructor(basePubSubResult: BasePubSubResult) : this( + basePubSubResult.channel, + basePubSubResult.subscription, + basePubSubResult.timetoken, + basePubSubResult.userMetadata, + basePubSubResult.publisher + ) +} + diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/MessageResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/MessageResult.kt new file mode 100644 index 000000000..42af17567 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/MessageResult.kt @@ -0,0 +1,8 @@ +package com.pubnub.api.models.consumer.pubsub + +import com.google.gson.JsonElement + +open class MessageResult( + basePubSubResult: BasePubSubResult, + val message: JsonElement +) : BasePubSubResult(basePubSubResult) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNMessageResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNMessageResult.kt new file mode 100644 index 000000000..90a1ef695 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNMessageResult.kt @@ -0,0 +1,8 @@ +package com.pubnub.api.models.consumer.pubsub + +import com.google.gson.JsonElement + +class PNMessageResult( + basePubSubResult: BasePubSubResult, + messageResult: JsonElement +) : MessageResult(basePubSubResult, messageResult) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNPresenceEventResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNPresenceEventResult.kt new file mode 100644 index 000000000..498e8760a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNPresenceEventResult.kt @@ -0,0 +1,19 @@ +package com.pubnub.api.models.consumer.pubsub + +import com.google.gson.JsonElement + +class PNPresenceEventResult( + val event: String? = null, + val uuid: String? = null, + val timestamp: Long? = null, + val occupancy: Int? = null, + val state: JsonElement? = null, + val channel: String? = null, + val subscription: String? = null, + val timetoken: Long? = null, + val join: List? = null, + val leave: List? = null, + val timeout: List? = null, + val hereNowRefresh: Boolean? = null, + val userMetadata: Any? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNSignalResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNSignalResult.kt new file mode 100644 index 000000000..d5dc664f3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/PNSignalResult.kt @@ -0,0 +1,8 @@ +package com.pubnub.api.models.consumer.pubsub + +import com.google.gson.JsonElement + +class PNSignalResult( + basePubSubResult: BasePubSubResult, + messageResult: JsonElement +) : MessageResult(basePubSubResult, messageResult) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/message_actions/PNMessageActionResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/message_actions/PNMessageActionResult.kt new file mode 100644 index 000000000..f99c024d7 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/message_actions/PNMessageActionResult.kt @@ -0,0 +1,14 @@ +package com.pubnub.api.models.consumer.pubsub.message_actions + +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.consumer.pubsub.BasePubSubResult +import com.pubnub.api.models.consumer.pubsub.objects.ObjectResult + +class PNMessageActionResult( + result: BasePubSubResult, + override val event: String, + data: PNMessageAction +) : ObjectResult(result, event, data) { + + val messageAction = data +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectPayload.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectPayload.kt new file mode 100644 index 000000000..91e30c6ce --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectPayload.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.models.consumer.pubsub.objects + +import com.google.gson.JsonElement + +data class ObjectPayload( + val source: String, + val version: String, + val event: String, + val type: String, + val data: JsonElement +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectResult.kt b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectResult.kt new file mode 100644 index 000000000..1c5b395cf --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/pubsub/objects/ObjectResult.kt @@ -0,0 +1,9 @@ +package com.pubnub.api.models.consumer.pubsub.objects + +import com.pubnub.api.models.consumer.pubsub.BasePubSubResult + +open class ObjectResult( + internal open val result: BasePubSubResult, + internal open val event: String, + internal open val data: T +) : BasePubSubResult(result) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/push/PNPushResults.kt b/src/main/kotlin/com/pubnub/api/models/consumer/push/PNPushResults.kt new file mode 100644 index 000000000..63d4d8f13 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/push/PNPushResults.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.models.consumer.push + +class PNPushAddChannelResult + +class PNPushListProvisionsResult internal constructor( + val channels: List +) + +class PNPushRemoveAllChannelsResult + +class PNPushRemoveChannelResult \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt b/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt new file mode 100644 index 000000000..6773abf7d --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt @@ -0,0 +1,171 @@ +package com.pubnub.api.models.consumer.push.payload + +import com.pubnub.api.enums.PNPushEnvironment + +class PushPayloadHelper { + + var commonPayload: Map? = null + var fcmPayload: FCMPayload? = null + var mpnsPayload: MPNSPayload? = null + var apnsPayload: APNSPayload? = null + + fun build(): Map { + return mutableMapOf().apply { + apnsPayload?.let { + it.toMap().run { + if (isNotEmpty()) { + put("pn_apns", this) + } + } + } + fcmPayload?.let { + it.toMap().run { + if (isNotEmpty()) { + put("pn_gcm", this) + } + } + } + mpnsPayload?.let { + it.toMap().run { + if (isNotEmpty()) { + put("pn_mpns", this) + } + } + } + commonPayload?.let { putAll(it) } + } + } + + + class APNSPayload : PushPayloadSerializer { + var aps: APS? = null + var apns2Configurations: List? = null + var custom: Map? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + aps?.let { + it.toMap().run { + if (this.isNotEmpty()) { + put("aps", this) + } + } + } + apns2Configurations?.let { put("pn_push", it.map { it.toMap() }.filter { it.isNotEmpty() }.toList()) } + custom?.let { putAll(it) } + } + } + + class APS : PushPayloadSerializer { + var alert: Any? = null + var badge: Int? = null + var sound: String? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + alert?.let { put("alert", it) } + badge?.let { put("badge", it) } + sound?.let { put("sound", it) } + } + } + + } + + class APNS2Configuration : PushPayloadSerializer { + var collapseId: String? = null + var expiration: String? = null + var targets: List? = null + var version: String? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + collapseId?.let { put("collapse_id", it) } + expiration?.let { put("expiration", it) } + targets?.let { + it.map { it.toMap() }.filter { it.isNotEmpty() }.toList().run { + if (this.isNotEmpty()) { + put("targets", this) + } + } + } + version?.let { put("version", it) } + } + } + + class Target : PushPayloadSerializer { + var topic: String? = null + var excludeDevices: List? = null + var environment: PNPushEnvironment? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + topic?.let { put("topic", it) } + excludeDevices?.let { put("excluded_devices", it) } + environment?.let { put("environment", it.name.toLowerCase()) } + } + } + } + + } + + } + + class MPNSPayload : PushPayloadSerializer { + var count: Int? = null + var backTitle: String? = null + var title: String? = null + var backContent: String? = null + var type: String? = null + var custom: Map? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + count?.let { put("count", it) } + backTitle?.let { put("back_title", it) } + title?.let { put("title", it) } + backContent?.let { put("back_content", it) } + type?.let { put("type", it) } + custom?.let { putAll(it) } + } + } + + } + + class FCMPayload : PushPayloadSerializer { + var custom: Map? = null + var data: Map? = null + var notification: Notification? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + custom?.let { putAll(it) } + data?.let { + if (it.isNotEmpty()) { + put("data", it) + } + } + notification?.let { + it.toMap().run { + if (this.isNotEmpty()) { + put("notification", this) + } + } + } + } + } + + class Notification : PushPayloadSerializer { + var title: String? = null + var body: String? = null + var image: String? = null + + override fun toMap(): Map { + return mutableMapOf().apply { + title?.let { put("title", it) } + body?.let { put("body", it) } + image?.let { put("image", it) } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadSerializer.kt b/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadSerializer.kt new file mode 100644 index 000000000..0551378f5 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadSerializer.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.models.consumer.push.payload + +interface PushPayloadSerializer { + + fun toMap(): Map + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/DeleteMessagesEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/DeleteMessagesEnvelope.kt new file mode 100644 index 000000000..282653ba3 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/DeleteMessagesEnvelope.kt @@ -0,0 +1,10 @@ +package com.pubnub.api.models.server + +import com.google.gson.annotations.SerializedName + +class DeleteMessagesEnvelope( + val status: Int?, + val error: Boolean?, + @SerializedName("error_message") + val errorMessage: String? +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/Envelope.kt b/src/main/kotlin/com/pubnub/api/models/server/Envelope.kt new file mode 100644 index 000000000..957d849ff --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/Envelope.kt @@ -0,0 +1,14 @@ +package com.pubnub.api.models.server + +import com.google.gson.JsonElement + +class Envelope { + internal val status: Int = 0 + internal val message: String? = null + internal val service: String? = null + internal val payload: T? = null + internal val occupancy: Int = 0 + internal val uuids: JsonElement? = null + internal val action: String? = null + internal val error: Boolean = false +} diff --git a/src/main/kotlin/com/pubnub/api/models/server/FetchMessagesEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/FetchMessagesEnvelope.kt new file mode 100644 index 000000000..381b1ccb2 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/FetchMessagesEnvelope.kt @@ -0,0 +1,7 @@ +package com.pubnub.api.models.server + +import com.pubnub.api.models.consumer.history.PNFetchMessageItem + +class FetchMessagesEnvelope( + val channels: HashMap> +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/OriginationMetaData.kt b/src/main/kotlin/com/pubnub/api/models/server/OriginationMetaData.kt new file mode 100644 index 000000000..05a16eb43 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/OriginationMetaData.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.models.server + +import com.google.gson.annotations.SerializedName + +class OriginationMetaData( + @SerializedName("t") + internal val timetoken: Long?, + + @SerializedName("r") + internal val region: Int? +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/PresenceEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/PresenceEnvelope.kt new file mode 100644 index 000000000..ffd92d2a6 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/PresenceEnvelope.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.models.server + +import com.google.gson.JsonElement + +class PresenceEnvelope( + internal val action: String? = null, + internal val uuid: String? = null, + internal val occupancy: Int? = null, + internal val timestamp: Long? = null, + internal val data: JsonElement? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/PublishMetaData.kt b/src/main/kotlin/com/pubnub/api/models/server/PublishMetaData.kt new file mode 100644 index 000000000..873c8d68b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/PublishMetaData.kt @@ -0,0 +1,12 @@ +package com.pubnub.api.models.server + +import com.google.gson.annotations.SerializedName + +class PublishMetaData( + @SerializedName("t") + internal val publishTimetoken: Long?, + + @SerializedName("r") + internal val region: Int? +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/SubscribeEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/SubscribeEnvelope.kt new file mode 100644 index 000000000..b68070fdf --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/SubscribeEnvelope.kt @@ -0,0 +1,12 @@ +package com.pubnub.api.models.server + +import com.google.gson.annotations.SerializedName + +class SubscribeEnvelope( + + @SerializedName("m") + internal val messages: List, + + @SerializedName("t") + internal val metadata: SubscribeMetaData +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/SubscribeMessage.kt b/src/main/kotlin/com/pubnub/api/models/server/SubscribeMessage.kt new file mode 100644 index 000000000..d7ef19666 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/SubscribeMessage.kt @@ -0,0 +1,44 @@ +package com.pubnub.api.models.server + +import com.google.gson.JsonElement +import com.google.gson.annotations.SerializedName + +class SubscribeMessage( + @SerializedName("a") + internal val shard: String?, + + @SerializedName("b") + internal val subscriptionMatch: String?, + + @SerializedName("c") + internal val channel: String?, + + @SerializedName("d") + internal val payload: JsonElement?, + + @SerializedName("f") + internal val flags: String?, + + @SerializedName("i") + internal val issuingClientId: String?, + + @SerializedName("k") + internal val subscribeKey: String?, + + @SerializedName("o") + internal val originationMetadata: OriginationMetaData?, + + @SerializedName("p") + internal val publishMetaData: PublishMetaData?, + + @SerializedName("u") + internal val userMetadata: JsonElement?, + + @SerializedName("e") + internal val type: Int? + +) { + + fun supportsEncryption() = type == null || type == 0 + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/SubscribeMetaData.kt b/src/main/kotlin/com/pubnub/api/models/server/SubscribeMetaData.kt new file mode 100644 index 000000000..0c6a4d008 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/SubscribeMetaData.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.models.server + +import com.google.gson.annotations.SerializedName + +class SubscribeMetaData( + @SerializedName("t") + internal val timetoken: Long, + + @SerializedName("r") + internal val region: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/access_manager/AccessManagerGrantPayload.kt b/src/main/kotlin/com/pubnub/api/models/server/access_manager/AccessManagerGrantPayload.kt new file mode 100644 index 000000000..24c545138 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/access_manager/AccessManagerGrantPayload.kt @@ -0,0 +1,26 @@ +package com.pubnub.api.models.server.access_manager + +import com.google.gson.JsonElement +import com.google.gson.annotations.SerializedName +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerKeyData +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerKeysData + +class AccessManagerGrantPayload { + + internal var level: String? = null + + internal var ttl = 0 + + @SerializedName("subscribe_key") + internal var subscribeKey: String? = null + + internal val channels: Map? = null + + @SerializedName("channel-groups") + internal val channelGroups: JsonElement? = null + + @SerializedName("auths") + internal val authKeys: Map? = null + + internal val channel: String? = null +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityArrayEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityArrayEnvelope.kt new file mode 100644 index 000000000..63f832941 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityArrayEnvelope.kt @@ -0,0 +1,9 @@ +package com.pubnub.api.models.server.objects_api + +open class EntityArrayEnvelope : EntityEnvelope>() { + + var totalCount: Int = 0 + var next: String? = null + var prev: String? = null + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityEnvelope.kt b/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityEnvelope.kt new file mode 100644 index 000000000..070bebb3a --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/objects_api/EntityEnvelope.kt @@ -0,0 +1,8 @@ +package com.pubnub.api.models.server.objects_api + +open class EntityEnvelope { + var status = 0 + private set + var data: T? = null + internal set +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/models/server/presence/WhereNowPayload.kt b/src/main/kotlin/com/pubnub/api/models/server/presence/WhereNowPayload.kt new file mode 100644 index 000000000..fbcf4b94b --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/models/server/presence/WhereNowPayload.kt @@ -0,0 +1,3 @@ +package com.pubnub.api.models.server.presence + +class WhereNowPayload(val channels: List) \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/AccessManagerService.kt b/src/main/kotlin/com/pubnub/api/services/AccessManagerService.kt new file mode 100644 index 000000000..0dbd6033e --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/AccessManagerService.kt @@ -0,0 +1,18 @@ +package com.pubnub.api.services + +import com.pubnub.api.models.server.Envelope +import com.pubnub.api.models.server.access_manager.AccessManagerGrantPayload +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface AccessManagerService { + + @GET("/v2/auth/grant/sub-key/{subKey}") + fun grant( + @Path("subKey") subKey: String, + @QueryMap options: Map + ): Call> + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/ChannelGroupService.kt b/src/main/kotlin/com/pubnub/api/services/ChannelGroupService.kt new file mode 100644 index 000000000..637611621 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/ChannelGroupService.kt @@ -0,0 +1,45 @@ +package com.pubnub.api.services + +import com.pubnub.api.models.server.Envelope +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +internal interface ChannelGroupService { + + @GET("v1/channel-registration/sub-key/{subKey}/channel-group") + fun listAllChannelGroup( + @Path("subKey") subKey: String, + @QueryMap options: Map + ): Call>> + + @GET("v1/channel-registration/sub-key/{subKey}/channel-group/{group}") + fun allChannelsChannelGroup( + @Path("subKey") subKey: String, + @Path("group") group: String, + @QueryMap options: Map + ): Call>> + + @GET("v1/channel-registration/sub-key/{subKey}/channel-group/{group}") + fun addChannelChannelGroup( + @Path("subKey") subKey: String, + @Path("group") group: String, + @QueryMap options: Map + ): Call + + @GET("v1/channel-registration/sub-key/{subKey}/channel-group/{group}") + fun removeChannel( + @Path("subKey") subKey: String, + @Path("group") group: String, + @QueryMap options: Map + ): Call + + @GET("v1/channel-registration/sub-key/{subKey}/channel-group/{group}/remove") + fun deleteChannelGroup( + @Path("subKey") subKey: String, + @Path("group") group: String, + @QueryMap options: Map + ): Call + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/HistoryService.kt b/src/main/kotlin/com/pubnub/api/services/HistoryService.kt new file mode 100644 index 000000000..e27775a3c --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/HistoryService.kt @@ -0,0 +1,48 @@ +package com.pubnub.api.services + +import com.google.gson.JsonElement +import com.pubnub.api.models.server.FetchMessagesEnvelope +import retrofit2.Call +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface HistoryService { + + @GET("v2/history/sub-key/{subKey}/channel/{channel}") + fun fetchHistory( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call + + @DELETE("v3/history/sub-key/{subKey}/channel/{channels}") + fun deleteMessages( + @Path("subKey") subKey: String, + @Path("channels") channels: String, + @QueryMap options: Map + ): Call + + @GET("v3/history/sub-key/{subKey}/channel/{channels}") + fun fetchMessages( + @Path("subKey") subKey: String, + @Path("channels") channels: String, + @QueryMap options: Map + ): Call + + @GET("v3/history-with-actions/sub-key/{subKey}/channel/{channel}") + fun fetchMessagesWithActions( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call + + @GET("v3/history/sub-key/{subKey}/message-counts/{channels}") + fun fetchCount( + @Path("subKey") subKey: String, + @Path("channels") channels: String, + @QueryMap options: Map + ): Call + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/MessageActionService.kt b/src/main/kotlin/com/pubnub/api/services/MessageActionService.kt new file mode 100644 index 000000000..eed6422d7 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/MessageActionService.kt @@ -0,0 +1,36 @@ +package com.pubnub.api.services + +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.server.objects_api.EntityEnvelope +import retrofit2.Call +import retrofit2.http.* + +interface MessageActionService { + + @POST("v1/message-actions/{subKey}/channel/{channel}/message/{messageTimetoken}") + @Headers("Content-Type: application/json; charset=UTF-8") + fun addMessageAction( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path("messageTimetoken") messageTimetoken: String, + @Body body: Any, + @QueryMap options: Map + ): Call> + + @GET("v1/message-actions/{subKey}/channel/{channel}") + fun getMessageActions( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call>> + + @DELETE("v1/message-actions/{subKey}/channel/{channel}/message/{messageTimetoken}/action/{actionTimetoken}") + fun deleteMessageAction( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path("messageTimetoken") messageTimetoken: String, + @Path("actionTimetoken") actionTimetoken: String, + @QueryMap options: Map + ): Call + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/PresenceService.kt b/src/main/kotlin/com/pubnub/api/services/PresenceService.kt new file mode 100644 index 000000000..703dc853c --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/PresenceService.kt @@ -0,0 +1,63 @@ +package com.pubnub.api.services + +import com.google.gson.JsonElement +import com.pubnub.api.models.server.Envelope +import com.pubnub.api.models.server.presence.WhereNowPayload +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface PresenceService { + + @GET("v2/presence/sub-key/{subKey}/channel/{channel}/leave") + fun leave( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call + + @GET("v2/presence/sub-key/{subKey}/channel/{channel}/heartbeat") + fun heartbeat( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call + + @GET("v2/presence/sub-key/{subKey}/uuid/{uuid}") + fun whereNow( + @Path("subKey") subKey: String, + @Path("uuid") uuid: String, + @QueryMap options: Map + ): Call> + + @GET("v2/presence/sub_key/{subKey}/channel/{channel}") + fun hereNow( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call> + + @GET("v2/presence/sub_key/{subKey}") + fun globalHereNow( + @Path("subKey") subKey: String, + @QueryMap options: Map + ): Call> + + @GET("v2/presence/sub-key/{subKey}/channel/{channel}/uuid/{uuid}") + fun getState( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path("uuid") uuid: String, + @QueryMap options: Map + ): Call> + + @GET("v2/presence/sub-key/{subKey}/channel/{channel}/uuid/{uuid}/data") + fun setState( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path("uuid") uuid: String, + @QueryMap options: Map + ): Call> + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/PublishService.kt b/src/main/kotlin/com/pubnub/api/services/PublishService.kt new file mode 100644 index 000000000..e53a0e3eb --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/PublishService.kt @@ -0,0 +1,26 @@ +package com.pubnub.api.services + +import retrofit2.Call +import retrofit2.http.* + +interface PublishService { + + @GET("publish/{pubKey}/{subKey}/0/{channel}/0/{message}") + fun publish( + @Path("pubKey") pubKey: String, + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path(value = "message", encoded = false) message: String, + @QueryMap(encoded = false) options: Map + ): Call> + + @POST("publish/{pubKey}/{subKey}/0/{channel}/0") + @Headers("Content-Type: application/json; charset=UTF-8") + fun publishWithPost( + @Path("pubKey") pubKey: String, + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Body body: Any, + @QueryMap(encoded = false) options: Map + ): Call> +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/PushService.kt b/src/main/kotlin/com/pubnub/api/services/PushService.kt new file mode 100644 index 000000000..39f0ce67c --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/PushService.kt @@ -0,0 +1,54 @@ +package com.pubnub.api.services + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface PushService { + + @GET("v1/push/sub-key/{subKey}/devices/{pushToken}") + fun modifyChannelsForDevice( + @Path("subKey") subKey: String, + @Path("pushToken") pushToken: String, + @QueryMap options: Map + ): Call + + @GET("v1/push/sub-key/{subKey}/devices/{pushToken}/remove") + fun removeAllChannelsForDevice( + @Path("subKey") subKey: String, + @Path("pushToken") pushToken: String, + @QueryMap options: Map + ): Call + + @GET("v1/push/sub-key/{subKey}/devices/{pushToken}") + fun listChannelsForDevice( + @Path("subKey") subKey: String, + @Path("pushToken") pushToken: String, + @QueryMap options: Map + ): Call> + + // V2 (APNS2) + + // V2 (APNS2) + @GET("v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2}") + fun modifyChannelsForDeviceApns2( + @Path("subKey") subKey: String, + @Path("deviceApns2") deviceApns2: String, + @QueryMap options: Map + ): Call + + @GET("v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2}") + fun listChannelsForDeviceApns2( + @Path("subKey") subKey: String, + @Path("deviceApns2") deviceApns2: String, + @QueryMap options: Map + ): Call> + + @GET("v2/push/sub-key/{subKey}/devices-apns2/{deviceApns2}/remove") + fun removeAllChannelsForDeviceApns2( + @Path("subKey") subKey: String, + @Path("deviceApns2") deviceApns2: String, + @QueryMap options: Map + ): Call +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/SignalService.kt b/src/main/kotlin/com/pubnub/api/services/SignalService.kt new file mode 100644 index 000000000..a11d1134f --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/SignalService.kt @@ -0,0 +1,19 @@ +package com.pubnub.api.services + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface SignalService { + + @GET("/signal/{pubKey}/{subKey}/0/{channel}/0/{payload}") + fun signal( + @Path("pubKey") pubKey: String, + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @Path(value = "payload") message: String, + @QueryMap options: Map + ): Call> + +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/SubscribeService.kt b/src/main/kotlin/com/pubnub/api/services/SubscribeService.kt new file mode 100644 index 000000000..b95241165 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/SubscribeService.kt @@ -0,0 +1,16 @@ +package com.pubnub.api.services + +import com.pubnub.api.models.server.SubscribeEnvelope +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface SubscribeService { + @GET("v2/subscribe/{subKey}/{channel}/0") + fun subscribe( + @Path("subKey") subKey: String, + @Path("channel") channel: String, + @QueryMap options: Map + ): Call +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/services/TimeService.kt b/src/main/kotlin/com/pubnub/api/services/TimeService.kt new file mode 100644 index 000000000..4aa343fbb --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/services/TimeService.kt @@ -0,0 +1,11 @@ +package com.pubnub.api.services + +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.QueryMap + +interface TimeService { + + @GET("/time/0") + fun fetchTime(@QueryMap options: Map): Call> +} \ No newline at end of file diff --git a/src/main/kotlin/com/pubnub/api/workers/SubscribeMessageWorker.kt b/src/main/kotlin/com/pubnub/api/workers/SubscribeMessageWorker.kt new file mode 100644 index 000000000..4d99115e1 --- /dev/null +++ b/src/main/kotlin/com/pubnub/api/workers/SubscribeMessageWorker.kt @@ -0,0 +1,226 @@ +package com.pubnub.api.workers + +import com.google.gson.JsonElement +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubException +import com.pubnub.api.PubNubUtil +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.managers.DuplicationManager +import com.pubnub.api.managers.ListenerManager +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.consumer.pubsub.BasePubSubResult +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import com.pubnub.api.models.consumer.pubsub.objects.ObjectPayload +import com.pubnub.api.models.server.PresenceEnvelope +import com.pubnub.api.models.server.SubscribeMessage +import com.pubnub.api.vendor.Crypto +import org.slf4j.LoggerFactory +import java.util.concurrent.LinkedBlockingQueue + +internal class SubscribeMessageWorker( + val pubnub: PubNub, + val listenerManager: ListenerManager, + val queue: LinkedBlockingQueue, + val duplicationManager: DuplicationManager +) : Runnable { + + private val log = LoggerFactory.getLogger("SubscribeMessageWorker") + + private var running = false + + companion object { + private const val TYPE_MESSAGE = 0 + private const val TYPE_SIGNAL = 1 + private const val TYPE_OBJECT = 2 + private const val TYPE_MESSAGE_ACTION = 3 + } + + override fun run() { + takeMessage() + } + + private fun takeMessage() { + running = true + + while (running) { + try { + processIncomingPayload(queue.take()) + } catch (e: InterruptedException) { + running = false + log.trace("take message interrupted!") + e.printStackTrace() + } + } + } + + private fun processIncomingPayload(message: SubscribeMessage) { + if (message.channel == null) { + return + } + + val channel = message.channel + var subscriptionMatch = message.subscriptionMatch + val publishMetaData = message.publishMetaData + + if (channel == subscriptionMatch) { + subscriptionMatch = null + } + + if (pubnub.configuration.dedupOnSubscribe) { + if (duplicationManager.isDuplicate(message)) { + return + } else { + duplicationManager.addEntry(message) + } + } + + if (message.channel.endsWith("-pnpres")) { + val presencePayload = pubnub.mapper.convertValue(message.payload, PresenceEnvelope::class.java) + val strippedPresenceChannel = PubNubUtil.replaceLast(channel, "-pnpres", "") + val strippedPresenceSubscription = subscriptionMatch?.let { + PubNubUtil.replaceLast(it, "-pnpres", "") + } + + val isHereNowRefresh = message.payload?.asJsonObject?.get("here_now_refresh") + + listenerManager.announce( + PNPresenceEventResult( + event = presencePayload.action, + uuid = presencePayload.uuid, + timestamp = presencePayload.timestamp, + occupancy = presencePayload.occupancy, + state = presencePayload.data, + channel = strippedPresenceChannel, + subscription = strippedPresenceSubscription, + timetoken = publishMetaData?.publishTimetoken, + join = getDelta(message.payload?.asJsonObject?.get("join")), + leave = getDelta(message.payload?.asJsonObject?.get("leave")), + timeout = getDelta(message.payload?.asJsonObject?.get("timeout")), + hereNowRefresh = isHereNowRefresh != null && isHereNowRefresh.asBoolean + ) + ) + } else { + val extractedMessage = processMessage(message) + + if (extractedMessage == null) { + log.debug("unable to parse payload on #processIncomingMessages") + } + + val result = BasePubSubResult( + channel = channel, + subscription = subscriptionMatch, + timetoken = publishMetaData?.publishTimetoken, + userMetadata = message.userMetadata, + publisher = message.issuingClientId + ) + + when (message.type) { + null -> { + listenerManager.announce(PNMessageResult(result, extractedMessage!!)) + } + TYPE_MESSAGE -> { + listenerManager.announce(PNMessageResult(result, extractedMessage!!)) + } + TYPE_SIGNAL -> { + listenerManager.announce(PNSignalResult(result, extractedMessage!!)) + } + TYPE_OBJECT -> { + + } + TYPE_MESSAGE_ACTION -> { + val objectPayload = pubnub.mapper.convertValue(extractedMessage, ObjectPayload::class.java) + val data = objectPayload.data.asJsonObject + if (!data.has("uuid")) { + data.addProperty("uuid", result.publisher) + } + listenerManager.announce( + PNMessageActionResult( + result = result, + event = objectPayload.event, + data = pubnub.mapper.convertValue(data, PNMessageAction::class.java) + ) + ) + } + } + } + + } + + private fun processMessage(subscribeMessage: SubscribeMessage): JsonElement? { + val input = subscribeMessage.payload + + // if we do not have a crypto key, there is no way to process the node; let's return. + if (!pubnub.configuration.isCipherKeyValid()) { + return input + } + + // if the message couldn't possibly be encrypted in the first place, there is no way to process the node; + // let's return. + if (!subscribeMessage.supportsEncryption()) { + return input + } + + val crypto = Crypto(pubnub.configuration.cipherKey) + + val inputText = + if (pubnub.mapper.isJsonObject(input!!) && pubnub.mapper.hasField(input, "pn_other")) { + pubnub.mapper.elementToString(input, "pn_other") + } else { + pubnub.mapper.elementToString(input) + } + + val outputText = + try { + crypto.decrypt(inputText!!) + } catch (e: PubNubException) { + val pnStatus = PNStatus( + PNStatusCategory.PNMalformedResponseCategory, + true, + PNOperationType.PNSubscribeOperation, + e + ) + listenerManager.announce(pnStatus) + return null + } + + var outputObject = + try { + pubnub.mapper.fromJson(outputText, JsonElement::class.java) + } catch (e: PubNubException) { + val pnStatus = PNStatus( + PNStatusCategory.PNMalformedResponseCategory, + true, + PNOperationType.PNSubscribeOperation, + e + ) + listenerManager.announce(pnStatus) + return null + } + + if (pubnub.mapper.isJsonObject(input) && pubnub.mapper.hasField(input, "pn_other")) { + val objectNode = pubnub.mapper.getAsObject(input) + pubnub.mapper.putOnObject(objectNode, "pn_other", outputObject) + outputObject = objectNode + } + + return outputObject + } + + private fun getDelta(delta: JsonElement?): List { + val list = mutableListOf() + delta?.let { + it.asJsonArray.forEach { item: JsonElement? -> + item?.let { + list.add(it.asString) + } + } + } + return list + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/Utils.kt b/src/test/kotlin/com/pubnub/api/Utils.kt new file mode 100644 index 000000000..21465ed63 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/Utils.kt @@ -0,0 +1,175 @@ +package com.pubnub.api + +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.verification.LoggedRequest +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.pubnub.api.models.consumer.PNStatus +import okhttp3.HttpUrl +import okhttp3.Request +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import java.io.BufferedReader +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.stream.Collectors + +var DEFAULT_LISTEN_DURATION: Int? = null + +private fun observe(success: AtomicBoolean, seconds: Int) { + Awaitility.await() + .atMost(seconds.toLong(), TimeUnit.SECONDS) + .untilTrue(success) +} + +fun AtomicBoolean.listen(seconds: Int = DEFAULT_LISTEN_DURATION!!) { + observe(this, seconds) +} + +fun AtomicBoolean.listen(function: () -> Boolean): AtomicBoolean { + Awaitility.await() + .atMost(Durations.FIVE_SECONDS) + .with() + .until { + function.invoke() + } + return this +} + +fun assertPnException(expectedPubNubError: PubNubError, pnStatus: PNStatus) { + Assertions.assertTrue(pnStatus.error) + assertEquals(expectedPubNubError, pnStatus.exception!!.pubnubError) +} + +fun assertPnException(expectedPubNubError: PubNubError, exception: Exception) { + exception as PubNubException + assertEquals(expectedPubNubError, exception.pubnubError) +} + +fun PNStatus.param(param: String) = clientRequest!!.url().queryParameter(param) + +fun PNStatus.encodedParam(param: String) = clientRequest!!.url().encodedQuery()!!.encodedParamString(param) + +fun String.encodedParamString(param: String): String { + return split("&") + .first { it.startsWith(param) } + .split("=")[1] +} + +fun emptyJson() = WireMock.aResponse().withBody("{}") + +fun failTest(message: String? = null) { + Assertions.fail(message) +} + +private fun decomposeAndVerifySignature( + configuration: PNConfiguration, + url: String, + method: String, + body: String = "" +) { + val httpUrl = HttpUrl.parse(url) + println(httpUrl) + + val sortedQueryString = httpUrl!!.run { + queryParameterNames() + .filter { it != "signature" } + .map { it to PubNubUtil.pamEncode(queryParameterValues(it).first()) } + .toMap() + .toSortedMap() + .map { "${it.key}=${it.value}" } + .joinToString("&") + } + + var v2Signature = true + + val input = + if (!(httpUrl.encodedPath().startsWith("/publish") && method.toLowerCase() == "post")) { + """ + ${method.toUpperCase()} + ${configuration.publishKey} + ${httpUrl.encodedPath()} + $sortedQueryString + $body + """.trimIndent() + } else { + v2Signature = false + """ + ${configuration.subscribeKey} + ${configuration.publishKey} + ${httpUrl.encodedPath()} + $sortedQueryString + """.trimIndent() + } + + val actualSignature = httpUrl.queryParameter("signature") + val verifiedSignature = verifyViaPython(configuration.secretKey, input, v2Signature) + + val rebuiltSignature = PubNubUtil.signSHA256(configuration.secretKey, input).run { + if (v2Signature) "v2.${this.trim('=')}" + else this + } + + println("originalTimestamp:\t${httpUrl.queryParameter("timestamp")}") + println("signatureInput:\t$input") + println("actualSignature:\t$actualSignature") + println("rebuiltSignature:\t$rebuiltSignature") + println("verifiedSignature:\t$verifiedSignature") + + assertEquals(actualSignature, rebuiltSignature) + assertEquals(actualSignature, verifiedSignature) +} + +fun decomposeAndVerifySignature(configuration: PNConfiguration, request: LoggedRequest) { + decomposeAndVerifySignature( + configuration = configuration, + url = request.absoluteUrl, + method = request.method.name, + body = request.bodyAsString + ) +} + +fun decomposeAndVerifySignature(configuration: PNConfiguration, request: Request) { + decomposeAndVerifySignature( + configuration = configuration, + url = request.url().toString(), + method = request.method(), + body = PubNubUtil.requestBodyToString(request)!! + ) +} + +private fun verifyViaPython(key: String, input: String, v2Signature: Boolean): String { + val i = input.replace("\n", "###") + val path = ClassLoader.getSystemClassLoader().getResource("hmacsha256.py")!!.path + val command = "python $path $key $i $v2Signature" + val process = Runtime.getRuntime().exec(command) + return BufferedReader(InputStreamReader(process.inputStream)).readLine() +} + +fun getSpecialCharsMap(): List { + val resourceFileAsString = getResourceFileAsString("special_chars.json") + return Gson().fromJson(resourceFileAsString, object : TypeToken>() {}.type) +} + +fun getResourceFileAsString(fileName: String?): String? { + val classLoader = ClassLoader.getSystemClassLoader() + classLoader.getResourceAsStream(fileName).use { inputStream -> + if (inputStream == null) return null + InputStreamReader(inputStream).use { isr -> + BufferedReader(isr).use { reader -> + return reader.lines().collect(Collectors.joining(System.lineSeparator())) + } + } + } +} + +fun Any.stringify() = Gson().toJson(this) + +class SpecialChar( + val name: String, + val regular: String, + val encoded: String +) \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/AppTest.kt b/src/test/kotlin/com/pubnub/api/integration/AppTest.kt new file mode 100644 index 000000000..8b0a3ff04 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/AppTest.kt @@ -0,0 +1,197 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PNConfiguration +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNLogVerbosity +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNOperationType.PNTimeOperation +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.enums.PNStatusCategory.PNUnknownCategory +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.TestInstance.Lifecycle +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean + +@TestInstance(Lifecycle.PER_CLASS) +class AppTest { + + lateinit var pubnub: PubNub + lateinit var pubKey: String + lateinit var subKey: String + + @BeforeAll + fun extractProperties() { + val inputStream = javaClass.classLoader.getResourceAsStream("config.properties") + val properties = Properties() + properties.load(inputStream) + pubKey = properties.getProperty("pub_key") + subKey = properties.getProperty("sub_key") + } + + @BeforeEach + fun initPubnub() { + pubnub = PubNub( + PNConfiguration().apply { + subscribeKey = subKey + publishKey = pubKey + logVerbosity = PNLogVerbosity.BODY + } + ) + } + + @AfterEach + fun cleanUp() { + pubnub.forceDestroy() + } + + @Test + fun testPublishSync() { + pubnub.publish().apply { + channel = UUID.randomUUID().toString() + message = UUID.randomUUID().toString() + }.sync().let { + Assertions.assertNotNull(it) + } + } + + @Test + fun testPublishAsync() { + val success = AtomicBoolean() + + pubnub.publish().apply { + channel = UUID.randomUUID().toString() + message = UUID.randomUUID().toString() + }.async { result, status -> + assertFalse(status.error) + result!!.timetoken + success.set(true) + } + + success.listen() + + success.set(false) + + Thread.sleep(2000) + + pubnub.publish().apply { + channel = UUID.randomUUID().toString() + message = UUID.randomUUID().toString() + }.async { result, status -> + assertFalse(status.error) + result!!.timetoken + success.set(true) + } + + success.listen() + } + + @Test + fun testSubscribe() { + val success = AtomicBoolean() + val expectedChannel = UUID.randomUUID().toString() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + assertTrue(pnStatus.operation == PNOperationType.PNSubscribeOperation) + assertTrue(pnStatus.category == PNStatusCategory.PNConnectedCategory) + assertTrue(pnStatus.affectedChannels.contains(expectedChannel)) + success.set(true) + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {} + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) {} + override fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) {} + }) + + val subscribe = pubnub.subscribe() + subscribe.channels = listOf(expectedChannel) + subscribe.withPresence = true + + subscribe.execute() + + success.listen() + } + + @Test + fun testSynchronizedAccess() { + val size = 500 + var counter = 0 + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + counter++ + val time = SimpleDateFormat("HH:mm:ss:SSS").format(System.currentTimeMillis()) + println("$time ${pnStatus.authKey} [$counter]") + } + }) + + val pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + + repeat(size) { + pool.execute { + pubnub.subscriptionManager.listenerManager.announce( + PNStatus( + category = PNUnknownCategory, + error = false, + operation = PNTimeOperation + ).apply { + authKey = Thread.currentThread().name + } + ) + } + } + + Awaitility.await() + .atMost(Durations.TEN_SECONDS) + .conditionEvaluationListener { + if (it.remainingTimeInMS < 300) { + println("Almost done. Counter value: $counter") + } + } + .until { counter == size } + } + + @Test + fun testHereNow() { + val expectedChannels = listOf(UUID.randomUUID().toString()) + + pubnub.subscribe().apply { + channels = expectedChannels + withPresence = true + }.execute() + + Awaitility.await() + .atMost(Durations.FIVE_SECONDS) + .pollDelay(Durations.ONE_SECOND) + .pollInterval(Durations.ONE_SECOND) + .with() + .until { + pubnub.whereNow().apply { + uuid = pubnub.configuration.uuid + }.sync()!! + .channels + .containsAll(expectedChannels) + } + + pubnub.hereNow().apply { + channels = expectedChannels + includeUUIDs = false + includeState = false + }.sync() + } + +} diff --git a/src/test/kotlin/com/pubnub/api/integration/BaseIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/BaseIntegrationTest.kt new file mode 100644 index 000000000..d68ffa5ab --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/BaseIntegrationTest.kt @@ -0,0 +1,363 @@ +package com.pubnub.api.integration + +import com.google.gson.JsonObject +import com.pubnub.api.DEFAULT_LISTEN_DURATION +import com.pubnub.api.PNConfiguration +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNLogVerbosity +import com.pubnub.api.models.consumer.PNPublishResult +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.json.JSONObject +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInstance +import java.util.* +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class BaseIntegrationTest { + + companion object { + const val TIMEOUT_MEDIUM = 5 + const val TIMEOUT_LOW = 2 + lateinit var PUB_KEY: String + lateinit var SUB_KEY: String + lateinit var PAM_PUB_KEY: String + lateinit var PAM_SUB_KEY: String + lateinit var PAM_SEC_KEY: String + } + + lateinit var pubnub: PubNub + lateinit var server: PubNub + + private var mGuestClients = mutableListOf() + + @BeforeAll + fun extractProperties() { + val inputStream = javaClass.classLoader.getResourceAsStream("config.properties") + val properties = Properties() + properties.load(inputStream) + PUB_KEY = properties.getProperty("pub_key") + SUB_KEY = properties.getProperty("sub_key") + PAM_PUB_KEY = properties.getProperty("pam_pub_key") + PAM_SUB_KEY = properties.getProperty("pam_sub_key") + PAM_SEC_KEY = properties.getProperty("pam_sec_key") + DEFAULT_LISTEN_DURATION = 5 + } + + @BeforeEach + private fun before() { + onPrePubnub() + pubnub = createPubNub() + if (needsServer()) { + server = createServer() + } + onBefore() + } + + @AfterEach + open fun after() { + onAfter() + destroyClient(pubnub) + for (guestClient in mGuestClients) { + destroyClient(guestClient) + } + } + + protected fun createPubNub(): PubNub { + var pnConfiguration = provideStagingConfiguration() + if (pnConfiguration == null) { + pnConfiguration = getBasicPnConfiguration() + } + val pubNub = PubNub(pnConfiguration) + registerGuestClient(pubNub) + return pubNub + } + + private fun createServer(): PubNub { + val pubNub = PubNub(getServerPnConfiguration()) + registerGuestClient(pubNub) + return pubNub + } + + private fun registerGuestClient(guestClient: PubNub) { + mGuestClients.add(guestClient) + } + + protected open fun getBasicPnConfiguration(): PNConfiguration { + val pnConfiguration = PNConfiguration() + if (!needsServer()) { + pnConfiguration.subscribeKey = SUB_KEY + pnConfiguration.publishKey = PUB_KEY + } else { + pnConfiguration.subscribeKey = PAM_SUB_KEY + pnConfiguration.publishKey = PAM_PUB_KEY + pnConfiguration.authKey = provideAuthKey()!! + } + pnConfiguration.logVerbosity = PNLogVerbosity.BODY + pnConfiguration.uuid = "client-${UUID.randomUUID()}" + return pnConfiguration + } + + private fun getServerPnConfiguration(): PNConfiguration { + val pnConfiguration = PNConfiguration() + pnConfiguration.subscribeKey = PAM_SUB_KEY + pnConfiguration.publishKey = PAM_PUB_KEY + pnConfiguration.secretKey = PAM_SEC_KEY + pnConfiguration.logVerbosity = PNLogVerbosity.BODY + pnConfiguration.uuid = "server-${UUID.randomUUID()}" + return pnConfiguration + } + + private fun destroyClient(client: PubNub) { + client.unsubscribeAll() + client.forceDestroy() + } + + private fun needsServer() = provideAuthKey() != null + + protected open fun onBefore() {} + protected open fun onAfter() {} + protected open fun onPrePubnub() {} + + protected open fun provideAuthKey(): String? { + return null + } + + protected open fun provideStagingConfiguration(): PNConfiguration? { + return null + } + + fun wait(seconds: Int = 3) { + Thread.sleep((seconds * 1_000).toLong()) + /*Awaitility.await() + .atMost(seconds.toLong(), TimeUnit.SECONDS) + .until { true }*/ + } + + /*protected fun subscribeToChannel(vararg channels: String, pubnub: PubNub = this.pubnub) { + pubnub.subscribe().apply { + this.channels = listOf(*channels) + withPresence = true + + }.execute() + + wait(2) + }*/ + +} + +internal fun PubNub.subscribeToBlocking(vararg channels: String) { + this.subscribe().apply { + this.channels = listOf(*channels) + withPresence = true + }.execute() + Thread.sleep(2000) +} + +internal fun PubNub.unsubscribeFromBlocking(vararg channels: String) { + unsubscribe().apply { + this.channels = listOf(*channels) + }.execute(); + Thread.sleep(2000) +} + +fun randomValue(length: Int = 10): String { + return ((0..9).toList().map(Int::toString) + ('A'..'Z').toList().map(Char::toString)) + .shuffled() + .toList() + .take(length) + .joinToString(separator = "") +} + +fun randomNumeric(length: Int = 10): String { + return generateSequence { (0..9).random() }.take(length).toList().shuffled().joinToString(separator = "") +} + +fun publishMixed(pubnub: PubNub, count: Int, channel: String): List { + val list = mutableListOf() + repeat(count) { + val sync = pubnub.publish().apply { + this.channel = channel + this.message = "${it}_msg" + val meta = when { + it % 2 == 0 -> generateMap() + it % 3 == 0 -> randomValue(4) + else -> null + } + meta?.let { this.meta = it } + + }.sync() + list.add(sync!!) + } + return list +} + +fun generateMap() = mapOf( + "text" to randomValue(8), + "uncd" to unicode(), + "info" to randomValue(8) +) + +fun generatePayload(): JsonObject { + return JsonObject().apply { + addProperty("text", randomValue()) + addProperty("uncd", unicode(8)) + addProperty("info", randomValue()) + } +} + +fun generateMessage(pubnub: PubNub): JsonObject { + return JsonObject().apply { + addProperty("publisher", pubnub.configuration.uuid) + addProperty("text", randomValue()) + addProperty("uncd", unicode(8)) + + } +} + +fun generatePayloadJSON(): JSONObject { + return JSONObject().apply { + put("text", randomValue()) + put("uncd", unicode(8)) + put("info", randomValue()) + } +} + +fun unicode(length: Int = 5): String { + val unicodeChars = "!?+-=" + return unicodeChars.toList().shuffled().take(length).joinToString(separator = "") +} + +fun emoji(): String { + return listOf( + "😀", + "😁", + "😂", + "🤣", + "😃", + "😄", + "😅", + "😆", + "😉", + "😊", + "😋", + "😎", + "😍", + "😘", + "🥰", + "😗", + "😙", + "😚", + "☺️", + "🙂", + "🤗", + "🤩", + "🤔", + "🤨", + "😐", + "😑", + "😶", + "🙄", + "😏", + "😣", + "😥", + "😮", + "🤐", + "😯", + "😪", + "😫", + "😴", + "😌", + "😛", + "😜", + "😝", + "🤤", + "😒", + "😓", + "😔", + "😕", + "🙃", + "🤑", + "😲", + "☹️", + "🙁", + "😖", + "😞", + "😟", + "😤", + "😢", + "😭", + "😦", + "😧", + "😨", + "😩", + "🤯", + "😬", + "😰", + "😱", + "🥵", + "🥶", + "😳", + "🤪", + "😵", + "😡", + "😠", + "🤬", + "😷", + "🤒", + "🤕", + "🤢", + "🤮", + "🤧", + "😇", + "🤠", + "🤡", + "🥳", + "🥴", + "🥺", + "🤥", + "🤫", + "🤭", + "🧐", + "🤓", + "😈", + "👿", + "👹", + "👺", + "💀", + "👻", + "👽", + "🤖", + "💩", + "😺", + "😸", + "😹", + "😻", + "😼", + "😽", + "🙀", + "😿", + "😾" + ).shuffled().take(5).joinToString("") +} + +inline fun retry(crossinline block: () -> Unit) { + Awaitility.await() + .pollInterval(Durations.TWO_HUNDRED_MILLISECONDS) + .atLeast(Durations.FIVE_SECONDS) + .until { + val latch = CountDownLatch(1) + try { + block.invoke() + } catch (e: Exception) { + e.printStackTrace() + } + latch.countDown() + latch.await(1, TimeUnit.SECONDS) + } +} diff --git a/src/test/kotlin/com/pubnub/api/integration/EncodingIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/EncodingIntegrationTest.kt new file mode 100644 index 000000000..55435135a --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/EncodingIntegrationTest.kt @@ -0,0 +1,153 @@ +package com.pubnub.api.integration + +import com.pubnub.api.decomposeAndVerifySignature +import com.pubnub.api.encodedParam +import com.pubnub.api.getSpecialCharsMap +import com.pubnub.api.listen +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.stream.Stream + +class EncodingIntegrationTest : BaseIntegrationTest() { + + @Test + fun testSpecialCharsPathAndUrl() { + val expectedChannel = randomValue() + val keyName = "special_char" + val value = getSpecialCharsMap().map { it.regular }.shuffled().joinToString("") + + val success = AtomicBoolean() + + server.publish().apply { + channel = expectedChannel + message = value + meta = mapOf( + keyName to value + ) + queryParam = mapOf( + "za" to value, + "aa" to value, + "s" to value, + "Zz" to value, + "ZZZ" to value, + "123" to value + ) + }.async { _, status -> + assertFalse(status.error) + decomposeAndVerifySignature(server.configuration, status.clientRequest!!) + success.set(true) + } + + success.listen() + + Awaitility.await() + .pollInterval(Durations.FIVE_HUNDRED_MILLISECONDS) + .atMost(Durations.FIVE_SECONDS) + .until { + val messages = server.history().apply { + channel = expectedChannel + includeMeta = true + }.sync()!!.messages + + assertEquals(1, messages.size) + assertEquals(value, messages[0].meta!!.asJsonObject[keyName].asString) + assertEquals(value, messages[0].entry.asString) + true + } + + } + + @DisplayName("EncodingTestSuite") + @ParameterizedTest(name = "testWith \"{0}\" {1}") + @MethodSource("provideSpecialCharsStream") + fun testVerifySignature(name: String, regular: String, encoded: String) { + val success = AtomicBoolean() + + server.configuration.includeRequestIdentifier = false + server.configuration.includeInstanceIdentifier = false + + val expectedChannel = randomValue() + val expectedMessage = "msg${regular}msg" + val expectedMetadata = "meta${regular}meta" + + val propertyName = "target" + + server.publish().apply { + channel = expectedChannel + message = expectedMessage + meta = expectedMetadata + queryParam = mapOf(propertyName to regular) + usePost = false + }.async { _, status -> + assertFalse(status.error) + val encodedParam = status.encodedParam(propertyName) + assertEquals(encoded, encodedParam) + decomposeAndVerifySignature(server.configuration, status.clientRequest!!) + success.set(true) + } + + success.listen() + + val historyTest = { + Awaitility.await() + .pollInterval(Durations.FIVE_HUNDRED_MILLISECONDS) + .atMost(Durations.FIVE_SECONDS) + .until { + val latch = CountDownLatch(1) + server.history().apply { + channel = expectedChannel + includeMeta = true + }.async { result, status -> + assertEquals(1, result!!.messages.size) + decomposeAndVerifySignature(server.configuration, status.clientRequest!!) + assertFalse(status.error) + assertEquals( + expectedMessage, + result.messages[0].entry.asString + ) + assertEquals( + expectedMetadata, + result.messages[0].meta!!.asString + ) + assertEquals( + regular, + result.messages[0].entry.asString.replace("msg", "") + ) + assertEquals( + regular, + result.messages[0].meta!!.asString.replace("meta", "") + ) + latch.countDown() + } + latch.await(1, TimeUnit.SECONDS) + } + } + + historyTest.invoke() + } + + companion object { + + @JvmStatic + fun provideSpecialCharsStream(): Stream { + return getSpecialCharsMap() + .map { Arguments.of(it.name, it.regular, it.encoded) } + .toList() + .shuffled() + .stream() + } + } + + override fun provideAuthKey() = "" + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt new file mode 100644 index 000000000..7cdc3bb08 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/GroupManagementIntegrationTests.kt @@ -0,0 +1,116 @@ +package com.pubnub.api.integration + +import com.pubnub.api.suite.await +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class GroupManagementIntegrationTests : BaseIntegrationTest() { + + lateinit var expectedChannel1: String + lateinit var expectedChannel2: String + lateinit var expectedChannel3: String + + lateinit var expectedGroup: String + + override fun onBefore() { + expectedChannel1 = randomValue() + expectedChannel2 = randomValue() + expectedChannel3 = randomValue() + expectedGroup = randomValue(8) + } + + @Test + fun testRemoveChannelsFromGroup() { + addChannelsToGroup() + + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = expectedGroup + channels = listOf(expectedChannel1, expectedChannel2, expectedChannel3) + }.await { _, status -> + assertFalse(status.error) + assertEquals(3, status.affectedChannels.size) + assertEquals(0, pubnub.getSubscribedChannels().size) + assertEquals(1, status.affectedChannelGroups.size) + assertEquals(0, pubnub.getSubscribedChannelGroups().size) + } + } + + @Test + fun testRemoveChannelFromGroup() { + addChannelsToGroup() + + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = expectedGroup + channels = listOf(expectedChannel1) + }.await { _, status -> + assertFalse(status.error) + } + } + + @Test + fun testSubscribeToChannelGroup() { + addChannelsToGroup() + + pubnub.subscribe().apply { + channelGroups = listOf(expectedGroup) + withPresence = true + }.execute() + + wait() + + assertTrue(pubnub.getSubscribedChannelGroups().contains(expectedGroup)) + } + + @Test + fun testUnsubscribeFromChannelGroup() { + addChannelsToGroup() + + pubnub.subscribe().apply { + channelGroups = listOf(expectedGroup) + withPresence = true + }.execute() + + wait() + + pubnub.unsubscribe().apply { + channelGroups = listOf(expectedGroup) + }.execute() + + wait() + + assertTrue(pubnub.getSubscribedChannelGroups().isEmpty()) + } + + @Test + fun testGetAllChannelsFrogroup() { + addChannelsToGroup() + + pubnub.listChannelsForChannelGroup().apply { + channelGroup = expectedGroup + }.await { result, status -> + assertFalse(status.error) + assertEquals(3, result!!.channels.size) + } + } + + @Test + fun testAddChannelsToGroup() { + pubnub.addChannelsToChannelGroup().apply { + channelGroup = expectedGroup + channels = listOf(expectedChannel1, expectedChannel2, expectedChannel3) + }.await { _, status -> + assertFalse(status.error) + assertEquals(1, status.affectedChannelGroups.size) + assertEquals(3, status.affectedChannels.size) + } + } + + private fun addChannelsToGroup() { + pubnub.addChannelsToChannelGroup().apply { + channelGroup = expectedGroup + channels = listOf(expectedChannel1, expectedChannel2, expectedChannel3) + }.await { _, status -> + assertFalse(status.error) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/HeartbeatIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/HeartbeatIntegrationTest.kt new file mode 100644 index 000000000..bca323af7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/HeartbeatIntegrationTest.kt @@ -0,0 +1,103 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class HeartbeatIntegrationTest : BaseIntegrationTest() { + + lateinit var expectedChannel: String + + override fun onBefore() { + expectedChannel = randomValue() + } + + @Test + fun testStateWithHeartbeat() { + val hits = AtomicInteger() + val expectedStatePayload = generatePayload() + + val observer = createPubNub().apply { + configuration.uuid = "observer_${System.currentTimeMillis()}" + } + + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatInterval = 4 + + observer.addListener(object : SubscribeCallback() { + + override fun status(p: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation + && pnStatus.affectedChannels.contains(expectedChannel) + ) { + pubnub.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute() + } + } + + override fun presence(p: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (pnPresenceEventResult.uuid.equals(pubnub.configuration.uuid) + && pnPresenceEventResult.channel.equals(expectedChannel) + ) { + when (pnPresenceEventResult.event) { + "state-change" -> { + assertEquals(expectedStatePayload, pnPresenceEventResult.state) + hits.incrementAndGet() + pubnub.disconnect() + } + "join" -> { + if (pnPresenceEventResult.state == null) { + hits.incrementAndGet() + val stateSet = AtomicBoolean() + pubnub.setPresenceState().apply { + state = expectedStatePayload + channels = Arrays.asList(expectedChannel) + + }.async { result, status -> + assertFalse(status.error) + assertEquals(expectedStatePayload, result!!.state) + hits.incrementAndGet() + stateSet.set(true) + } + + Awaitility.await().atMost(Durations.FIVE_SECONDS).untilTrue(stateSet) + } else { + assertEquals(expectedStatePayload, pnPresenceEventResult.state) + hits.incrementAndGet() + } + + } + "timeout" -> { + pubnub.reconnect(); + } + } + } + } + + }) + + observer.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute(); + + Awaitility.await() + .atMost(40, TimeUnit.SECONDS) + .untilAtomic(hits, IsEqual.equalTo(4)); + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt new file mode 100644 index 000000000..5410afba0 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/HistoryIntegrationTest.kt @@ -0,0 +1,532 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.param +import com.pubnub.api.suite.await +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class HistoryIntegrationTest : BaseIntegrationTest() { + + @Test + fun testHistorySingleChannel() { + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + pubnub.history().apply { + channel = expectedChannelName + }.sync()!!.run { + messages.forEach { + assertNotNull(it.entry) + assertNull(it.meta) + assertNull(it.timetoken) + } + } + } + + @Test + fun testHistorySingleChannel_Timetoken() { + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + pubnub.history().apply { + channel = expectedChannelName + includeTimetoken = true + }.sync()!!.run { + this.messages.forEach { + assertNotNull(it.entry) + assertNotNull(it.timetoken) + assertNull(it.meta) + } + } + } + + @Test + fun testHistorySingleChannel_Meta() { + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + pubnub.history().apply { + channel = expectedChannelName + includeMeta = true + }.sync()!!.run { + messages.forEach { + assertNotNull(it.entry) + assertNull(it.timetoken) + assertNotNull(it.meta) + } + } + } + + @Test + fun testHistorySingleChannel_Meta_Timetoken() { + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + pubnub.history().apply { + channel = expectedChannelName + includeMeta = true + includeTimetoken = true + }.sync()!!.run { + messages.forEach { + assertNotNull(it.entry) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + } + } + } + + @Test + fun testFetchSingleChannel() { + val expectedChannelName = randomValue() + + publishMixed(pubnub, 10, expectedChannelName) + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannelName) + maximumPerChannel = 25 + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNull(it.meta) + assertNull(it.actions) + } + } + } + + @Test + fun testFetchSingleChannel_Meta() { + val expectedChannelName = randomValue() + publishMixed(pubnub, 10, expectedChannelName) + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannelName) + maximumPerChannel = 25 + includeMeta = true + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + assertNull(it.actions) + } + } + } + + @Test + fun testFetchSingleChannel_Actions() { + val expectedChannelName = randomValue() + val results = publishMixed(pubnub, 120, expectedChannelName) + + // todo check with executorservice + + pubnub.addMessageAction().apply { + channel = expectedChannelName + messageAction = (PNMessageAction( + type = "reaction", + value = emoji(), + messageTimetoken = results[0].timetoken + )) + }.sync()!! + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannelName) + maximumPerChannel = 25 + includeMessageActions = true + includeMeta = false + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNull(it.meta) + if (it.timetoken == results[0].timetoken) { + assertNotNull(it.actions) + } else { + assertTrue(it.actions!!.isEmpty()) + } + } + } + } + + @Test + fun testFetchSingleChannel_ActionsMeta() { + val expectedChannelName = randomValue() + val results = publishMixed(pubnub, 2, expectedChannelName) + + pubnub.addMessageAction().apply { + channel = expectedChannelName + messageAction = PNMessageAction( + type = "reaction", + value = emoji(), + messageTimetoken = results[0].timetoken + ) + }.sync()!! + + wait() + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannelName) + maximumPerChannel = 25 + includeMessageActions = true + includeMeta = true + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + if (it.timetoken == results[0].timetoken) { + assertNotNull(it.actions) + } else { + assertTrue(it.actions!!.isEmpty()) + } + } + } + } + + @Test + fun testFetchMultiChannel() { + val expectedChannelNames = generateSequence { randomValue() } + .take(2).toList() + .also { + it.forEach { channel -> + publishMixed(pubnub, 10, channel) + } + } + + pubnub.fetchMessages().apply { + channels = expectedChannelNames + maximumPerChannel = 25 + }.sync()!!.run { + expectedChannelNames.forEach { expectedChannel -> + channels[expectedChannel]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNull(it.meta) + assertNull(it.actions) + } + } + } + + } + + @Test + fun testFetchSingleChannel_NoLimit() { + val expectedChannelName = randomValue() + assertEquals(10, publishMixed(pubnub, 10, expectedChannelName).size) + + wait() + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannelName) + }.sync()!!.run { + assertEquals(1, channels[expectedChannelName]!!.size) + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNull(it.meta) + assertNull(it.actions) + } + } + } + + @Test + fun testFetchSingleChannel_OverflowLimit() { + val expectedChannelName = randomValue() + + assertEquals(10, publishMixed(pubnub, 10, expectedChannelName).size) + + wait() + + pubnub.fetchMessages().apply { + channels = (listOf(expectedChannelName)) + maximumPerChannel = 100 + }.sync()!!.apply { + assertEquals(10, channels[expectedChannelName]!!.size) + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNull(it.meta) + assertNull(it.actions) + } + } + } + + @Test + fun testHistorySingleChannel_IncludeAll_Crypto() { + val expectedCipherKey = randomValue() + pubnub.configuration.cipherKey = expectedCipherKey + + val observer = createPubNub().apply { + configuration.cipherKey = expectedCipherKey + } + + assertEquals(pubnub.configuration.cipherKey, observer.configuration.cipherKey) + + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + observer.history().apply { + channel = expectedChannelName + includeTimetoken = true + includeMeta = true + }.sync()!!.run { + messages.forEach { + assertNotNull(it.entry) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + assertTrue(it.entry.toString().contains("_msg")) + } + } + } + + @Test + fun testFetchSingleChannel_IncludeAll_Crypto() { + val expectedCipherKey = randomValue() + pubnub.configuration.cipherKey = expectedCipherKey + + val observer = createPubNub().apply { + configuration.cipherKey = expectedCipherKey + } + + assertEquals(pubnub.configuration.cipherKey, observer.configuration.cipherKey) + + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + assertEquals( + expectedMessageCount, + publishMixed(pubnub, expectedMessageCount, expectedChannelName).size + ) + + observer.fetchMessages().apply { + channels = listOf(expectedChannelName) + includeMeta = true + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + assertNull(it.actions) + assertTrue(it.message.toString().contains("_msg")) + } + } + } + + @Test + fun testFetchSingleChannel_WithActions_IncludeAll_Crypto() { + val expectedCipherKey = randomValue() + pubnub.configuration.cipherKey = expectedCipherKey + + val observer = createPubNub().apply { + configuration.cipherKey = expectedCipherKey + } + + assertEquals(pubnub.configuration.cipherKey, observer.configuration.cipherKey) + + val expectedChannelName = randomValue() + val expectedMessageCount = 10 + + val mixed = publishMixed(pubnub, expectedMessageCount, expectedChannelName) + assertEquals(expectedMessageCount, mixed.size) + + val messagesWithActions = mutableListOf() + + mixed.forEachIndexed { i, it -> + if (i % 2 == 0) { + val reaction = pubnub.addMessageAction().apply { + channel = expectedChannelName + messageAction = PNMessageAction( + type = "reaction", + value = emoji(), + messageTimetoken = it.timetoken + ) + }.sync()!! + messagesWithActions.add(reaction.messageTimetoken) + } + } + + observer.fetchMessages().apply { + channels = listOf(expectedChannelName) + includeMeta = true + includeMessageActions = true + }.sync()!!.run { + channels[expectedChannelName]!!.forEach { + assertNotNull(it.message) + assertNotNull(it.timetoken) + assertNotNull(it.meta) + if (messagesWithActions.contains(it.timetoken)) { + assertNotNull(it.actions) + } else { + assertTrue(it.actions!!.isEmpty()) + } + assertTrue(it.message.toString().contains("_msg")) + } + } + } + + @Test + fun testFetchMultiChannel_WithMessageActions_Exception() { + try { + pubnub.fetchMessages().apply { + channels = listOf(randomValue(), randomValue()) + includeMessageActions = true + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.HISTORY_MESSAGE_ACTIONS_MULTIPLE_CHANNELS, e) + } + } + + @Test + fun testFetchSingleChannel_NoActions_Limit_Default() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + }.await { _, status -> + assertEquals("1", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_NoActions_Limit_Low() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + maximumPerChannel = -1 + }.await { _, status -> + assertEquals("1", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_NoActions_Limit_Valid() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + maximumPerChannel = 15 + }.await { _, status -> + assertEquals("15", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_NoActions_Limit_High() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + maximumPerChannel = 100 + }.await { _, status -> + assertEquals("25", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_WithActions_Limit_Default() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + includeMessageActions = true + }.await { _, status -> + assertEquals("25", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_WithActions_Limit_Low() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + includeMessageActions = true + maximumPerChannel = -1 + }.await { _, status -> + assertEquals("25", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_WithActions_Limit_High() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + includeMessageActions = true + maximumPerChannel = 200 + }.await { _, status -> + assertEquals("25", status.param("max")) + } + } + + @Test + fun testFetchSingleChannel_WithActions_Limit_Valid() { + pubnub.fetchMessages().apply { + channels = listOf(randomValue()) + includeMessageActions = true + maximumPerChannel = 15 + }.await { result, status -> + assertEquals("15", status.param("max")) + } + } + + @Test + fun testEmptyMeta() { + val expectedChannel = randomValue() + + // publish a message without any metadata + pubnub.publish().apply { + message = randomValue() + channel = expectedChannel + }.sync()!! + + wait() + + // /v2/history + pubnub.history().apply { + channel = expectedChannel + includeMeta = true + }.sync()!!.run { + assertEquals(1, messages.size) + assertNotNull(messages[0].meta) + } + + // /v3/history + pubnub.fetchMessages().apply { + channels = listOf(expectedChannel) + includeMeta = true + }.sync()!!.run { + assertEquals(1, channels[expectedChannel]!!.size) + assertNotNull(channels[expectedChannel]!![0].meta) + } + + // /v3/history-with-actions + pubnub.fetchMessages().apply { + channels = listOf(expectedChannel) + includeMeta = true + includeMessageActions = true + }.sync()!!.run { + assertEquals(1, channels[expectedChannel]!!.size) + assertNotNull(channels[expectedChannel]!![0].meta) + } + + // three responses from three different APIs will return a non-null meta field + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/MessageActionsIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/MessageActionsIntegrationTest.kt new file mode 100644 index 000000000..dd32844e6 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/MessageActionsIntegrationTest.kt @@ -0,0 +1,572 @@ +package com.pubnub.api.integration + +import com.pubnub.api.* +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.endpoints.message_actions.GetMessageActions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.history.Action +import com.pubnub.api.models.consumer.history.PNFetchMessageItem +import com.pubnub.api.models.consumer.history.PNFetchMessagesResult +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import com.pubnub.api.suite.await +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer + +class MessageActionsIntegrationTest : BaseIntegrationTest() { + + lateinit var publishResult: PNPublishResult + lateinit var expectedChannel: String + + override fun onBefore() { + expectedChannel = randomValue() + + publishResult = pubnub.publish().apply { + channel = expectedChannel + message = generatePayload() + }.sync()!! + } + + @Test + fun testAddMessageAction() { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = + PNMessageAction( + type = "someother", + value = "smiley", + messageTimetoken = publishResult.timetoken + ) + }.await { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAddMessageAction, status.operation) + + } + } + + @Test + fun testGetMessageAction() { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = + PNMessageAction( + type = randomValue(), + value = randomValue(), + messageTimetoken = publishResult.timetoken + ) + }.sync()!! + + pubnub.getMessageActions().apply { + channel = expectedChannel + }.await { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNGetMessageActions, status.operation) + } + } + + @Test + fun testDeleteMessageAction() { + val expectedValue = UUID.randomUUID().toString() + + val addMessageActionResult = pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = ( + PNMessageAction( + type = "REACTION", + value = expectedValue, + messageTimetoken = publishResult.timetoken + )) + }.sync()!! + + pubnub.removeMessageAction().apply { + messageTimetoken = publishResult.timetoken + actionTimetoken = addMessageActionResult.actionTimetoken + channel = expectedChannel + }.await { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNDeleteMessageAction, status.operation) + } + } + + @Test + fun testAddGetMessageAction() { + val expectedValue = UUID.randomUUID().toString() + + val addMessageActionResult = pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "REACTION", + value = expectedValue, + messageTimetoken = publishResult.timetoken + ) + }.sync() + + pubnub.getMessageActions().apply { + channel = expectedChannel + + }.await { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNGetMessageActions, status.operation) + assertEquals(1, result!!.actions.size) + assertEquals(result.actions[0].actionTimetoken, addMessageActionResult!!.actionTimetoken) + } + } + + @Test + fun testAddGetMessageAction_Bulk() { + val expectedMessageCount = 10 + val expectedChannel = randomValue() + + publishMixed(pubnub, expectedMessageCount, expectedChannel).forEach { pnPublishResult -> + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "reaction", + value = emoji(), + messageTimetoken = pnPublishResult.timetoken + ) + }.sync()!! + } + + pubnub.getMessageActions().apply { + channel = expectedChannel + }.await { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNGetMessageActions, status.operation) + assertEquals(expectedMessageCount, result!!.actions.size) + } + } + + @Test + @Throws(PubNubException::class) + fun testAddGetMessageAction_Bulk_Pagination() { + val expectedChannelName = randomValue() + val messageCount = 10 + val messages = publishMixed(pubnub, messageCount, expectedChannelName) + + assertEquals(10, messages.size) + + messages.forEachIndexed { index, i -> + pubnub.addMessageAction().apply { + channel = expectedChannelName + messageAction = PNMessageAction( + type = "reaction", + value = "${index + 1}_${emoji()}", + messageTimetoken = i.timetoken + ) + }.sync() + } + + val success = AtomicBoolean() + val count = AtomicInteger() + + page(expectedChannelName, System.currentTimeMillis() * 10_000, object : Callback { + override fun onMore(actions: List) { + count.set(count.get() + actions.size) + } + + override fun onDone() { + success.set(count.get() == messageCount) + } + }) + + success.listen() + } + + interface Callback { + fun onMore(actions: List) + fun onDone() + } + + fun page(channel: String, start: Long, callback: Callback) { + pubnub.getMessageActions().apply { + this.channel = channel + this.start = start + this.limit = 5 + + }.async { result, status -> + if (!status.error && !result!!.actions.isEmpty()) { + callback.onMore(result.actions) + page(channel, result.actions[0].actionTimetoken!!, callback) + } else { + callback.onDone() + } + } + } + + @Test + fun loopActions() { + val publishList = mutableListOf() + + val totalMessageCount = 10 + + for (i in 1..totalMessageCount) { + pubnub.publish().apply { + channel = expectedChannel + message = "${i}_${randomValue()}" + }.sync()!!.run { + publishList.add(this) + } + } + + assertEquals(totalMessageCount, publishList.size) + + publishList.forEachIndexed { i, it -> + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = + PNMessageAction( + type = "REACTION", + value = "${(i + 1)}_${randomValue(5)}", + messageTimetoken = it.timetoken + ) + }.sync()!! + } + + val pnActionList = mutableListOf() + + pageActions(3, expectedChannel, null, object : Callback { + override fun onMore(actions: List) { + pnActionList.addAll(actions.reversed()) + } + + override fun onDone() { + val tts = pnActionList.map { it.actionTimetoken!! }.toList() + assertTrue(tts == tts.sorted().reversed()) + } + }) + } + + fun pageActions(chunk: Int, channel: String, start: Long?, callback: Callback) { + val builder: GetMessageActions = pubnub.getMessageActions().apply { + this.limit = chunk + this.channel = channel + } + if (start != null) { + builder.start = start + } + val messageActionsResult = builder.sync() + if (messageActionsResult!!.actions.isNotEmpty()) { + callback.onMore(messageActionsResult.actions) + pageActions(chunk, channel, messageActionsResult.actions[0].actionTimetoken, callback) + } else { + callback.onDone() + } + } + + @Test + fun testFetchHistory() { + val expectedChannel = randomValue() + val expectedMessageCount = 10 + val publishResultList = publishMixed(pubnub, expectedMessageCount, expectedChannel) + + + publishResultList.forEachIndexed { i, it -> + if (i % 2 == 0 && i % 3 == 0) { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "receipt", + value = emoji(), + messageTimetoken = publishResultList[i].timetoken + ) + }.sync()!! + } + if (i % 3 == 0) { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "receipt", + value = emoji(), + messageTimetoken = publishResultList[i].timetoken + ) + }.sync()!! + } + if (i % 2 == 0) { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "receipt", + value = emoji(), + messageTimetoken = publishResultList[i].timetoken + ) + }.sync()!! + } + if (i % 5 == 0) { + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "fiver", + value = emoji(), + messageTimetoken = publishResultList[i].timetoken + ) + }.sync()!! + } + } + + val fetchMessagesResultWithActions: PNFetchMessagesResult = pubnub.fetchMessages().apply { + channels = Collections.singletonList(expectedChannel) + includeMeta = true + includeMessageActions = true + }.sync()!! + + fetchMessagesResultWithActions.channels.forEach { (channel, item: List) -> + println("Channel: " + channel + ". Messages: " + item.size) + item.forEach(Consumer { pnFetchMessageItem: PNFetchMessageItem -> + println("\tMessage: " + pnFetchMessageItem.message) + println("\tTimetoken: " + pnFetchMessageItem.timetoken) + println("\tMeta: " + pnFetchMessageItem.meta) + if (pnFetchMessageItem.actions == null) { + println("\t\tNo actions here.") + return@Consumer + } + println("\t\tTotal action types: " + pnFetchMessageItem.actions!!.size) + pnFetchMessageItem.actions!!.forEach { (type, map: HashMap>) -> + println("\t\t\tAction type: $type") + map.forEach { (value, actions: List) -> + println("\t\t\t\tAction value: $value") + actions.forEach(Consumer { action: Action -> + println("\t\t\t\tAction uuid: " + action.uuid) + println("\t\t\t\tAction timetoken: " + action.actionTimetoken) + }) + } + } + println("--------------------") + }) + } + fetchMessagesResultWithActions.channels.forEach { (s: String?, pnFetchMessageItems: List) -> + pnFetchMessageItems.forEach( + Consumer { pnFetchMessageItem: PNFetchMessageItem -> + assertNotNull(pnFetchMessageItem.actions) + assertTrue(pnFetchMessageItem.actions!!.size >= 0) + } + ) + } + val fetchMessagesResultNoActions: PNFetchMessagesResult = pubnub.fetchMessages().apply { + channels = listOf(expectedChannel) + }.sync()!! + fetchMessagesResultNoActions.channels.forEach { (s: String?, pnFetchMessageItems: List) -> + pnFetchMessageItems.forEach( + Consumer { pnFetchMessageItem: PNFetchMessageItem -> + assertNull( + pnFetchMessageItem.actions + ) + } + ) + } + } + + @Test + fun testActionReceive() { + val expectedChannelName = randomValue() + val expectedMessageCount = 1 + + val publishResultList = mutableListOf() + + repeat(expectedMessageCount) { + pubnub.publish().apply { + channel = expectedChannelName + message = "${it}_msg" + if (it % 2 == 0) meta = generateMap() + }.sync()!!.run { + publishResultList.add(this) + } + } + + assertEquals(expectedMessageCount, publishResultList.size) + + val actionsCount = AtomicInteger() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation) { + publishResultList.forEach { + pubnub.addMessageAction().apply { + channel = expectedChannelName + messageAction = PNMessageAction( + type = "reaction", + value = emoji(), + messageTimetoken = it.timetoken + ) + }.sync()!! + } + } + } + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + failTest() + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + failTest() + } + + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) { + failTest() + } + + override fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) { + assertEquals(expectedChannelName, pnMessageActionResult.channel) + actionsCount.incrementAndGet(); + } + }) + + pubnub.subscribe().apply { + channels = Collections.singletonList(expectedChannelName) + withPresence = true + + }.execute() + + Awaitility.await() + .atMost(Durations.TEN_SECONDS) + .untilAtomic(actionsCount, IsEqual.equalTo(expectedMessageCount)); + + } + + @Test + fun testAddAction_NoChannel() { + try { + pubnub.addMessageAction() + .sync() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testAddAction_NoMessageActionObject() { + try { + pubnub.addMessageAction().apply { + channel = randomValue() + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_MISSING, e) + } + } + + @Test + fun testAddAction_NoMessageActionType() { + try { + pubnub.addMessageAction().apply { + channel = randomValue() + messageAction = PNMessageAction( + type = "", + value = randomValue(), + messageTimetoken = 1L + ) + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_TYPE_MISSING, e) + } + } + + @Test + fun testAddAction_NoMessageActionValue() { + try { + pubnub.addMessageAction().apply { + channel = randomValue() + messageAction = PNMessageAction( + type = randomValue(), + value = "", + messageTimetoken = 1L + ) + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_VALUE_MISSING, e) + } + } + + @Test + fun testGetActions_NoChannel() { + try { + pubnub.getMessageActions() + .sync() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testRemoveAction_NoChannel() { + try { + pubnub.removeMessageAction() + .sync() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testRemoveAction_NoMessageTimetoken() { + try { + pubnub.removeMessageAction().apply { + channel = randomValue() + actionTimetoken = 1L + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_TIMETOKEN_MISSING, e) + } + } + + @Test + fun testRemoveAction_NoMessageActionTimetoken() { + try { + pubnub.removeMessageAction().apply { + channel = randomValue() + messageTimetoken = 1L + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_TIMETOKEN_MISSING, e) + } + } + + @Test + fun testAddSameActionTwice() { + val expectedChannel = randomValue() + val expectedEmoji = emoji() + + val timetoken = pubnub.publish().apply { + channel = expectedChannel + message = randomValue() + shouldStore = true + }.sync()!!.timetoken + + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "reaction", + value = expectedEmoji, + messageTimetoken = timetoken + ) + }.sync()!! + + pubnub.addMessageAction().apply { + channel = expectedChannel + messageAction = PNMessageAction( + type = "reaction", + value = expectedEmoji, + messageTimetoken = timetoken + ) + }.await { _, status -> + assertTrue(status.error) + assertEquals(409, status.statusCode) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/PresenceEventsIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/PresenceEventsIntegrationTests.kt new file mode 100644 index 000000000..7ff22ef61 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/PresenceEventsIntegrationTests.kt @@ -0,0 +1,119 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import org.awaitility.Awaitility +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class PresenceEventsIntegrationTests : BaseIntegrationTest() { + + lateinit var guest: PubNub + + override fun onBefore() { + guest = createPubNub() + } + + @Test + fun testJoinChannel() { + val success = AtomicBoolean() + val expectedChannel = randomValue() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + assertEquals("join", pnPresenceEventResult.event) + assertEquals(expectedChannel, pnPresenceEventResult.channel) + success.set(true) + } + + }) + + pubnub.subscribeToBlocking(expectedChannel) + + success.listen() + } + + @Test + fun testLeaveChannel() { + val success = AtomicBoolean() + val expectedChannel = randomValue() + + pubnub.subscribeToBlocking(expectedChannel) + guest.subscribeToBlocking(expectedChannel) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + assertEquals(expectedChannel, pnPresenceEventResult.channel) + assertEquals("leave", pnPresenceEventResult.event) + success.set(true) + } + }) + + guest.unsubscribeFromBlocking(expectedChannel) + + success.listen() + } + + @Test + fun testTimeoutFromChannel() { + val success = AtomicBoolean() + val expectedChannel = randomValue() + + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatInterval = 0 + + pubnub.addListener(object : SubscribeCallback() { + + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (pnPresenceEventResult.event == "timeout") { + assertEquals(expectedChannel, pnPresenceEventResult.channel) + success.set(true) + } + } + + }) + + pubnub.subscribeToBlocking(expectedChannel) + + Awaitility.await() + .atMost(21, TimeUnit.SECONDS) + .untilTrue(success) + } + + @Test + fun testStateChangeEvent() { + val success = AtomicBoolean() + val expectedChannel = randomValue() + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.addListener(object : SubscribeCallback() { + + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + assertEquals("state-change", pnPresenceEventResult.event) + assertEquals(pubnub.configuration.uuid, pnPresenceEventResult.uuid) + success.set(true) + } + + }) + + pubnub.setPresenceState().apply { + channels = listOf(expectedChannel) + state = generatePayload() + }.sync()!! + + success.listen() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt new file mode 100644 index 000000000..4537de842 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt @@ -0,0 +1,252 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNHeartbeatNotificationOptions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.suite.await + +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class PresenceIntegrationTests : BaseIntegrationTest() { + + @Test + fun testWhereNow() { + val expectedChannelsCount = 4 + val expectedChannels = generateSequence { randomValue() }.take(expectedChannelsCount).toList() + + pubnub.subscribeToBlocking(*expectedChannels.toTypedArray()) + + pubnub.whereNow().await { result, status -> + assertFalse(status.error) + assertEquals(expectedChannelsCount, result!!.channels.size) + assertEquals(expectedChannels.sorted(), result.channels.sorted()) + } + } + + @Test + fun testGlobalHereNow() { + val expectedChannelsCount = 2 + val expectedClientsCount = 3 + + val expectedChannels = generateSequence { randomValue() }.take(expectedChannelsCount).toList() + + val clients = mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub() }.take(expectedClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeToBlocking(*expectedChannels.toTypedArray()) + } + + wait(TIMEOUT_MEDIUM) + + pubnub.hereNow().apply { + includeUUIDs = true + }.await { result, status -> + assertFalse(status.error) + assertTrue(result!!.totalOccupancy >= expectedClientsCount) + assertTrue(result.totalChannels >= expectedChannelsCount) + assertTrue(result.channels.size >= expectedChannelsCount) + + assertTrue(result.channels.keys.containsAll(expectedChannels)) + + result.channels.forEach { (key, value) -> + if (expectedChannels.contains(key)) { + assertTrue(value.occupancy >= expectedClientsCount) + assertTrue(value.occupants.size >= expectedClientsCount) + + assertEquals( + clients.map { it.configuration.uuid }.toList(), + value.occupants.map { it.uuid }.toList() + ) + } + } + } + } + + @Test + fun testHereNow() { + val expectedChannelsCount = 2 + val expectedClientsCount = 3 + + val expectedChannels = generateSequence { randomValue() }.take(expectedChannelsCount).toList() + + val clients = mutableListOf(pubnub).apply { + addAll(generateSequence { createPubNub() }.take(expectedClientsCount - 1).toList()) + } + + clients.forEach { + it.subscribeToBlocking(*expectedChannels.toTypedArray()) + } + + assertEquals(expectedChannelsCount, expectedChannels.size) + assertEquals(expectedClientsCount, clients.size) + + wait(TIMEOUT_MEDIUM) + + pubnub.hereNow().apply { + channels = expectedChannels + includeUUIDs = true + }.await { result, status -> + assertFalse(status.error) + assertEquals(expectedChannelsCount, result!!.totalChannels) + assertEquals(expectedChannelsCount, result.channels.size) + assertEquals(expectedChannelsCount * expectedClientsCount, result.totalOccupancy) + result.channels.forEach { (key, value) -> + assertTrue(expectedChannels.contains(key)) + assertTrue(expectedChannels.contains(value.channelName)) + assertEquals(expectedClientsCount, value.occupancy) + assertEquals(expectedClientsCount, value.occupants.size) + value.occupants.forEach { occupant -> + val uuid = occupant.uuid + var contains = false + for (client in clients) { + if (client.configuration.uuid == uuid) { + contains = true + break + } + } + assertTrue(contains) + } + } + } + } + + @Test + fun testPresenceState() { + val hits = AtomicInteger() + val expectedHits = 2 + val expectedStatePayload = generatePayload() + val expectedChannel = randomValue() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (pnPresenceEventResult.event == "state-change" + && pnPresenceEventResult.channel == expectedChannel + && pnPresenceEventResult.uuid == pubnub.configuration.uuid + ) { + assertEquals(expectedStatePayload, pnPresenceEventResult.state) + hits.incrementAndGet() + } + } + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.setPresenceState().apply { + channels = listOf(expectedChannel) + state = expectedStatePayload + }.await { result, status -> + assertFalse(status.error) + assertEquals(expectedStatePayload, result!!.state) + } + + Awaitility.await() + .atMost(Durations.FIVE_SECONDS) + .untilAtomic(hits, IsEqual.equalTo(1)) + + pubnub.getPresenceState().apply { + channels = listOf(expectedChannel) + }.await { result, status -> + assertFalse(status.error) + assertEquals(expectedStatePayload, result!!.stateByUUID[expectedChannel]) + hits.incrementAndGet() + } + + Awaitility.await() + .atMost(Durations.FIVE_SECONDS) + .untilAtomic(hits, IsEqual.equalTo(expectedHits)) + } + + @Test + fun testHeartbeatsDisabled() { + val heartbeatCallsCount = AtomicInteger() + val subscribeSuccess = AtomicBoolean() + val expectedChannel = randomValue() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + assertEquals(PNHeartbeatNotificationOptions.ALL, pubnub.configuration.heartbeatNotificationOptions) + + pubnub.configuration.presenceTimeout = 20; + pubnub.configuration.heartbeatInterval = 0; + + assertEquals(20, pubnub.configuration.presenceTimeout) + assertEquals(0, pubnub.configuration.heartbeatInterval) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (!pnStatus.error && pnStatus.affectedChannels.contains(expectedChannel)) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation) { + subscribeSuccess.set(true) + } + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + heartbeatCallsCount.incrementAndGet() + } + } + } + }) + + pubnub.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute() + + Awaitility.await() + .atMost(20, TimeUnit.SECONDS) + .pollDelay(19, TimeUnit.SECONDS) + .until { + subscribeSuccess.get() && heartbeatCallsCount.get() == 0 + } + } + + @Test + fun testHeartbeatsEnabled() { + val heartbeatCallsCount = AtomicInteger() + val subscribeSuccess = AtomicBoolean() + val expectedChannel: String = randomValue() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + assertEquals(PNHeartbeatNotificationOptions.ALL, pubnub.configuration.heartbeatNotificationOptions) + + pubnub.configuration.presenceTimeout = 20 + + assertEquals(20, pubnub.configuration.presenceTimeout) + assertEquals(9, pubnub.configuration.heartbeatInterval) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + println(pnStatus.operation) + if (!pnStatus.error && pnStatus.affectedChannels.contains(expectedChannel)) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation) { + subscribeSuccess.set(true) + } + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + heartbeatCallsCount.incrementAndGet() + } + } + } + }) + + pubnub.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute() + + Awaitility.await() + .atMost(20, TimeUnit.SECONDS) + .until { + subscribeSuccess.get() && heartbeatCallsCount.get() > 2 + } + } +} diff --git a/src/test/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt new file mode 100644 index 000000000..f89f72f69 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/PublishIntegrationTests.kt @@ -0,0 +1,373 @@ +package com.pubnub.api.integration + +import com.google.gson.JsonObject +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.suite.await +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.hamcrest.core.IsEqual +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class PublishIntegrationTests : BaseIntegrationTest() { + + @Test + fun testPublishMessage() { + val expectedChannel = randomValue() + + pubnub.publish().apply { + channel = expectedChannel + message = generatePayload() + }.await { _, status -> + assertFalse(status.error) + assertEquals(status.uuid, pubnub.configuration.uuid) + } + } + + @Test + fun testPublishMessageHistory() { + val expectedChannel = randomValue() + + val expectedPayload = JSONObject().apply { + put("name", "joe") + put("age", 48) + } + + val convertedPayload = pubnub.mapper.convertValue(expectedPayload, JsonObject::class.java) + + pubnub.publish().apply { + channel = expectedChannel + message = expectedPayload + }.sync()!! + + wait() + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannel) + maximumPerChannel = 1 + + }.await { result, status -> + assertFalse(status.error) + assertEquals(1, result!!.channels.size) + assertEquals(1, result.channels[expectedChannel]!!.size) + assertEquals(convertedPayload, result.channels[expectedChannel]!![0].message) + } + } + + @Test + fun testPublishMessageNoHistory() { + val expectedChannel = randomValue() + val messagePayload = generateMessage(pubnub) + + pubnub.publish().apply { + message = messagePayload + channel = expectedChannel + shouldStore = false + }.await { _, status -> + assertFalse(status.error) + assertEquals(status.uuid, pubnub.configuration.uuid) + } + + wait() + + pubnub.history().apply { + count = 1 + channel = expectedChannel + }.await { result, status -> + assertFalse(status.error) + assertEquals(0, result!!.messages.size) + } + } + + @Test + fun testReceiveMessage() { + val success = AtomicBoolean() + val expectedChannel = randomValue() + val messagePayload = generateMessage(pubnub) + + val observer = createPubNub() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation + && pnStatus.affectedChannels.contains(expectedChannel) + ) { + observer.publish().apply { + message = messagePayload + channel = expectedChannel + }.async { _, status -> + assertFalse(status.error) + } + } + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + assertEquals(expectedChannel, pnMessageResult.channel) + assertEquals(observer.configuration.uuid, pnMessageResult.publisher) + assertEquals(messagePayload, pnMessageResult.message) + success.set(true) + } + + }) + + pubnub.subscribeToBlocking(expectedChannel) + + success.listen() + } + + @Test + fun testOrgJsonObject_Get_History() { + val expectedChannel = randomValue() + + val expectedPayload = JSONObject().apply { + put("name", "John Doe") + put("city", "San Francisco") + } + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + usePost = true + }.sync()!! + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 1 + }.sync()!!.run { + assertEquals(expectedPayload.toString(), JSONObject(messages[0].entry.toString()).toString()) + } + } + + @Test + fun testOrgJsonObject_Post_History() { + val expectedChannel = randomValue() + val expectedPayload = generatePayloadJSON() + + pubnub.publish().apply { + channel = expectedChannel + message = expectedPayload + usePost = true + }.sync()!! + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 1 + }.sync()!!.run { + assertEquals(expectedPayload.toString(), JSONObject(messages[0].entry.toString()).toString()) + } + } + + @Test + fun testOrgJsonObject_Get_Receive() { + val expectedChannel = randomValue() + val expectedPayload = generatePayloadJSON() + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + success.set(expectedPayload.toString() == JSONObject(pnMessageResult.message.toString()).toString()) + } + + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + }.sync()!! + + wait() + + success.listen() + } + + @Test + fun testOrgJsonObject_Post_Receive() { + val expectedChannel = randomValue() + val expectedPayload = generatePayloadJSON() + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + success.set(expectedPayload.toString() == JSONObject(pnMessageResult.message.toString()).toString()) + } + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + usePost = true + }.sync()!! + + success.listen() + } + + @Test + fun testOrgJsonArray_Get_History() { + val expectedChannel = randomValue() + val expectedPayload = JSONArray().apply { + repeat(2) { put(generatePayloadJSON()) } + } + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + }.sync()!! + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 1 + }.sync()!!.run { + assertEquals(expectedPayload.toString(), JSONArray(messages[0].entry.toString()).toString()) + } + } + + @Test + fun testOrgJsonArray_Post_History() { + val expectedChannel = randomValue() + val expectedPayload = JSONArray().apply { + repeat(2) { put(generatePayloadJSON()) } + } + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + usePost = true + }.sync()!! + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 1 + }.sync()!!.run { + assertEquals(expectedPayload.toString(), JSONArray(messages[0].entry.toString()).toString()) + } + } + + @Test + fun testOrgJsonArray_Get_Receive() { + val expectedChannel = randomValue() + val expectedPayload = JSONArray().apply { + repeat(2) { put(generatePayloadJSON()) } + } + + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + success.set(expectedPayload.toString() == JSONArray(pnMessageResult.message.toString()).toString()) + } + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + }.sync() + + success.listen() + } + + @Test + fun testOrgJsonArray_Post_Receive() { + val expectedChannel = randomValue() + val expectedPayload = JSONArray().apply { + repeat(2) { put(generatePayloadJSON()) } + } + + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + success.set(expectedPayload.toString() == JSONArray(pnMessageResult.message.toString()).toString()) + } + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.publish().apply { + message = expectedPayload + channel = expectedChannel + usePost = true + }.sync()!! + + success.listen() + } + + @Test + fun testOrgJson_Combo() { + val expectedChannel = randomValue() + + val expectedPayload = JSONObject().apply { + put("key_1", generatePayloadJSON()) + put("key_2", generatePayloadJSON()) + } + expectedPayload.put("z_1", JSONObject(expectedPayload.toString())) + expectedPayload.put("a_2", JSONObject(expectedPayload.toString())) + expectedPayload.put("d_3", JSONObject(expectedPayload.toString())) + expectedPayload.put("z_array", JSONArray().apply { repeat(3) { put(generatePayloadJSON()) } }) + + val count = AtomicInteger() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + assertEquals( + expectedPayload.toString(), + JSONObject(pnMessageResult.message.toString()).toString() + ) + count.incrementAndGet() + } + }) + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.publish().apply { + channel = expectedChannel + message = expectedPayload + usePost = true + }.sync() + + wait() + + pubnub.fetchMessages().apply { + channels = listOf(expectedChannel) + maximumPerChannel = 1 + }.sync()!!.run { + assertEquals( + expectedPayload.toString(), + JSONObject(channels[expectedChannel]!![0].message.toString()).toString() + ) + count.incrementAndGet() + } + + Awaitility.await() + .atMost(Durations.ONE_MINUTE) + .untilAtomic(count, IsEqual.equalTo(2)) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt new file mode 100644 index 000000000..69f61069b --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt @@ -0,0 +1,111 @@ +package com.pubnub.api.integration + +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* + +class PushIntegrationTest : BaseIntegrationTest() { + + lateinit var expectedChannels: List + lateinit var expectedDeviceId: String + lateinit var expectedTopic: String + + override fun onBefore() { + expectedChannels = + generateSequence { UUID.randomUUID().toString().substring(0, 8).toUpperCase() }.take(3).toList() + expectedDeviceId = + generateSequence { (0..9).random() }.take(70).toList().shuffled().joinToString(separator = "") + expectedTopic = UUID.randomUUID().toString() + } + + @Test + fun testEnumNames() { + assertEquals("apns", PNPushType.APNS.toParamString()) + assertEquals("gcm", PNPushType.FCM.toParamString()) + assertEquals("mpns", PNPushType.MPNS.toParamString()) + assertEquals("apns2", PNPushType.APNS2.toParamString()) + } + + @Test + fun testCycle() { + PNPushType.values().forEach { + runPushOperations(it) + } + } + + private fun runPushOperations(pushType: PNPushType) { + // todo logger + println("Push type '$pushType'") + println("Channels $expectedChannels'") + println("Topic $expectedTopic'") + println("GeneratedToken '${expectedDeviceId.length}': $expectedDeviceId") + + pubnub.addPushNotificationsOnChannels().apply { + channels = expectedChannels + this.pushType = pushType + topic = expectedTopic + deviceId = expectedDeviceId + + }.sync()!! + + wait() + + pubnub.auditPushChannelProvisions().apply { + deviceId = expectedDeviceId + this.pushType = pushType + topic = expectedTopic + environment = PNPushEnvironment.DEVELOPMENT + + }.sync()!!.run { + assertTrue(channels.containsAll(expectedChannels)) + } + + wait() + + pubnub.removePushNotificationsFromChannels().apply { + this.pushType = pushType + environment = PNPushEnvironment.DEVELOPMENT + deviceId = expectedDeviceId + topic = expectedTopic + channels = listOf(expectedChannels[0]) + + }.sync()!! + + wait() + + pubnub.auditPushChannelProvisions().apply { + deviceId = expectedDeviceId + this.pushType = pushType + topic = expectedTopic + environment = PNPushEnvironment.DEVELOPMENT + }.sync()!!.apply { + assertFalse(channels.isEmpty()) + assertFalse(channels.contains(expectedChannels[0])) + } + + wait() + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + this.pushType = pushType + environment = PNPushEnvironment.DEVELOPMENT + deviceId = expectedDeviceId + topic = expectedTopic + + }.sync()!! + + wait() + + pubnub.auditPushChannelProvisions().apply { + deviceId = expectedDeviceId + this.pushType = pushType + topic = expectedTopic + environment = PNPushEnvironment.DEVELOPMENT + }.sync()!!.apply { + assertTrue(channels.isEmpty()) + assertFalse(channels.containsAll(expectedChannels)) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt new file mode 100644 index 000000000..2d860bf39 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt @@ -0,0 +1,63 @@ +package com.pubnub.api.integration + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.models.consumer.PNStatus +import org.junit.jupiter.api.Test +import java.util.* + +class PushPayloadHelperIntegrationTest : BaseIntegrationTest() { + + @Test + fun testIntercept() { + val expectedChannel = UUID.randomUUID().toString() + + val payload = Gson().fromJson(json, JsonObject::class.java) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + pubnub.publish().apply { + channel = expectedChannel + message = payload + }.sync()!! + } + }) + + pubnub.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute() + + wait() + } + + val json = """ + { + "match": { + "tournament": "Barclay's Premier League", + "date": "2018-04-05 13:30:45", + "venue": "Anfield Road", + "title": "Goal! 90 min", + "summary": "Liverpool - Chelsea 2:1" + }, + "pn_gcm": { + "data": { + "title": "Goal! 90 min", + "summary": "Liverpool - Chelsea 2:1" + } + }, + "pn_apns": { + "aps": { + "title": "Goal! 90 min", + "summary": "Liverpool - Chelsea 2:1" + } + }, + "pn_mpns": { + "title": "Goal! 90 min", + "summary": "Liverpool - Chelsea 2:1" + } + } + """.trimIndent() +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/SignalIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/SignalIntegrationTests.kt new file mode 100644 index 000000000..a2656c256 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/SignalIntegrationTests.kt @@ -0,0 +1,121 @@ +package com.pubnub.api.integration + +import com.google.gson.Gson +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.suite.await +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class SignalIntegrationTests : BaseIntegrationTest() { + + lateinit var expectedChannel: String + lateinit var expectedPayload: String + + override fun onBefore() { + expectedChannel = randomValue() + expectedPayload = randomValue(5) + } + + @Test + fun testPublishSignalMessageAsync() { + pubnub.signal().apply { + message = expectedPayload + channel = expectedChannel + }.await { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNSignalOperation, status.operation) + assertEquals(status.uuid, pubnub.configuration.uuid) + assertNotNull(result) + } + } + + @Test + fun testPublishSignalMessageSync() { + pubnub.signal().apply { + message = expectedPayload + channel = expectedChannel + }.sync()!! + } + + @Test + fun testReceiveSignalMessage() { + val success = AtomicBoolean() + + val observerClient = createPubNub() + + observerClient.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation + && pnStatus.affectedChannels.contains(expectedChannel) + ) { + pubnub.signal().apply { + message = expectedPayload + channel = expectedChannel + }.async { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNSignalOperation, status.operation) + assertEquals(status.uuid, pubnub.configuration.uuid) + assertNotNull(result) + } + } + } + + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) { + assertEquals(pubnub.configuration.uuid, pnSignalResult.publisher) + assertEquals(expectedChannel, pnSignalResult.channel) + assertEquals(expectedPayload, Gson().fromJson(pnSignalResult.message, String::class.java)) + success.set(true) + } + }) + + observerClient.subscribe().apply { + channels = listOf(expectedChannel) + }.execute() + + success.listen() + } + + @Test + fun testPublishSignalMessageSyncWithoutChannel() { + try { + pubnub.signal().apply { + message = randomValue() + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testPublishSignalMessageSyncWithoutMessage() { + try { + pubnub.signal().apply { + channel = randomValue() + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_MISSING, e) + } + } + + @Test + fun testPublishSignalMessageSyncWithoutSubKey() { + try { + pubnub.configuration.subscribeKey = "" + pubnub.signal().apply { + channel = randomValue() + message = randomValue() + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/StorageAndPlaybackIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/StorageAndPlaybackIntegrationTests.kt new file mode 100644 index 000000000..6cb1d0acd --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/StorageAndPlaybackIntegrationTests.kt @@ -0,0 +1,132 @@ +package com.pubnub.api.integration + +import com.pubnub.api.suite.await +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class StorageAndPlaybackIntegrationTests : BaseIntegrationTest() { + + @Test + fun testHistoryMessages() { + val expectedMessage = randomValue() + val expectedChannel = randomValue() + + pubnub.publish().apply { + channel = expectedChannel + message = expectedMessage + }.await { _, _ -> } + + wait() + + pubnub.history().apply { + this.channel = expectedChannel + }.await { result, status -> + assertFalse(status.error) + assertEquals(expectedMessage, result!!.messages[0].entry.asString) + } + } + + @Test + fun testHistoryMessagesWithTimeToken() { + val expectedChannel = randomValue() + + repeat(3) { + pubnub.publish().apply { + channel = expectedChannel + message = randomValue() + }.sync()!! + } + + wait() + + pubnub.history().apply { + channel = expectedChannel + includeTimetoken = true + }.await { result, status -> + assertFalse(status.error) + result!!.messages.forEach { + assertNotNull(it.timetoken) + } + } + } + + @Test + fun testLoadingHistoryMessagesWithLimit() { + val expectedChannel = randomValue() + + repeat(20) { + pubnub.publish().apply { + channel = expectedChannel + message = randomValue() + }.sync()!! + } + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 10 + }.await { result, status -> + assertFalse(status.error) + assertEquals(10, result!!.messages.size) + } + } + + @Test + fun testLoadingHistoryWithSpecificTimeInterval() { + val expectedChannel = randomValue() + + val before = System.currentTimeMillis() * 10000 + wait(5) + repeat(3) { + pubnub.publish().apply { + channel = expectedChannel + message = randomValue() + }.sync()!! + } + wait(5) + val now = System.currentTimeMillis() * 10000 + + pubnub.history().apply { + channel = expectedChannel + includeTimetoken = true + start = now + end = before + count = 10 + }.await { result, status -> + assertFalse(status.error) + assertEquals(3, result!!.messages.size) + } + } + + @Test + fun testReverseHistoryPaging() { + val expectedChannel: String = randomValue() + val message1: String = randomValue(20) + val message2: String = randomValue(20) + + + pubnub.publish().apply { + channel = expectedChannel + message = message1 + }.sync()!! + + pubnub.publish().apply { + channel = expectedChannel + message = message2 + }.sync()!! + + wait() + + pubnub.history().apply { + channel = expectedChannel + count = 10 + reverse = true + }.await { result, status -> + assertFalse(status.error) + assertEquals(message1, result!!.messages[0].entry.asString) + assertEquals(message2, result.messages[1].entry.asString) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/integration/SubscribeIntegrationTests.kt b/src/test/kotlin/com/pubnub/api/integration/SubscribeIntegrationTests.kt new file mode 100644 index 000000000..9d42f7989 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/SubscribeIntegrationTests.kt @@ -0,0 +1,118 @@ +package com.pubnub.api.integration + +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class SubscribeIntegrationTests : BaseIntegrationTest() { + + lateinit var guestClient: PubNub + + override fun onBefore() { + guestClient = createPubNub() + } + + @Test + fun testSubscribeToMultipleChannels() { + val expectedChannelList = generateSequence { randomValue() }.take(3).toList() + + pubnub.subscribe().apply { + channels = expectedChannelList + withPresence = true + }.execute() + + wait() + + assertEquals(3, pubnub.getSubscribedChannels().size) + assertTrue(pubnub.getSubscribedChannels().contains(expectedChannelList[0])) + assertTrue(pubnub.getSubscribedChannels().contains(expectedChannelList[1])) + assertTrue(pubnub.getSubscribedChannels().contains(expectedChannelList[2])) + } + + @Test + fun testSubscribeToChannel() { + val expectedChannel = randomValue() + + pubnub.subscribe().apply { + channels = listOf(expectedChannel) + withPresence = true + }.execute() + + wait() + + assertEquals(1, pubnub.getSubscribedChannels().size) + assertTrue(pubnub.getSubscribedChannels().contains(expectedChannel)) + } + + @Test + fun testWildcardSubscribe() { + val success = AtomicBoolean() + + val expectedMessage = randomValue() + + pubnub.subscribeToBlocking("my.*") + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + assertEquals(expectedMessage, pnMessageResult.message.asString) + success.set(true) + } + }) + + guestClient.publish().apply { + channel = "my.test" + message = expectedMessage + }.sync()!! + + success.listen() + } + + @Test + fun testUnsubscribeFromChannel() { + val success = AtomicBoolean() + + val expectedChannel = randomValue() + + pubnub.subscribeToBlocking(expectedChannel) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNUnsubscribeOperation + && pnStatus.category == PNStatusCategory.PNAcknowledgmentCategory + ) { + success.set(pubnub.getSubscribedChannels().none { it == expectedChannel }) + } + } + }) + + pubnub.unsubscribeFromBlocking(expectedChannel) + + success.listen() + } + + @Test + fun testUnsubscribeFromAllChannels() { + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + success.set(pubnub.getSubscribedChannels().isEmpty()) + } + }) + + pubnub.unsubscribeAll() + + success.listen() + } + +} diff --git a/src/test/kotlin/com/pubnub/api/integration/TimeIntegrationTest.kt b/src/test/kotlin/com/pubnub/api/integration/TimeIntegrationTest.kt new file mode 100644 index 000000000..55a7530ca --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/integration/TimeIntegrationTest.kt @@ -0,0 +1,25 @@ +package com.pubnub.api.integration + +import com.pubnub.api.listen +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class TimeIntegrationTest : BaseIntegrationTest() { + + @Test + fun testGetPubNubTime() { + val success = AtomicBoolean() + + pubnub.time() + .async { result, status -> + assertFalse(status.error) + assertNotNull(result!!.timetoken) + success.set(true) + } + + success.listen() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/BaseTest.kt b/src/test/kotlin/com/pubnub/api/legacy/BaseTest.kt new file mode 100644 index 000000000..cda88f520 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/BaseTest.kt @@ -0,0 +1,60 @@ +package com.pubnub.api.legacy + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.pubnub.api.DEFAULT_LISTEN_DURATION +import com.pubnub.api.PNConfiguration +import com.pubnub.api.PubNub +import com.pubnub.api.enums.PNLogVerbosity +import okhttp3.HttpUrl +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach + +abstract class BaseTest { + + lateinit var wireMockServer: WireMockServer + protected lateinit var pubnub: PubNub + + @BeforeEach + fun beforeEach() { + // wireMockServer = WireMockServer(wireMockConfig().dynamicPort()) + wireMockServer = WireMockServer( + wireMockConfig() + .bindAddress("localhost") + .dynamicPort() + ) + wireMockServer.start() + WireMock.configureFor("http", "localhost", wireMockServer.port()) + pubnub = PubNub(PNConfiguration().apply { + subscribeKey = "mySubscribeKey" + publishKey = "myPublishKey" + uuid = "myUUID" + origin = HttpUrl.parse(wireMockServer.baseUrl())!!.run { "${host()}:${port()}" } + secure = false + logVerbosity = PNLogVerbosity.BODY + }) + DEFAULT_LISTEN_DURATION = 2 + onBefore() + } + + @AfterEach + fun afterEach() { + wireMockServer.stop() + wireMockServer.findAllUnmatchedRequests().forEach { + println("Unmatched ${it.url}") + } + assertTrue(wireMockServer.findAllUnmatchedRequests().isEmpty()) + onAfter() + pubnub.forceDestroy() + } + + open fun onBefore() { + + } + + open fun onAfter() { + + } +} diff --git a/src/test/kotlin/com/pubnub/api/legacy/PubNubTest.kt b/src/test/kotlin/com/pubnub/api/legacy/PubNubTest.kt new file mode 100644 index 000000000..2c399fbfe --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/PubNubTest.kt @@ -0,0 +1,85 @@ +package com.pubnub.api.legacy + +import com.pubnub.api.PNConfiguration +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNReconnectionPolicy +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class PubNubTest : BaseTest() { + + lateinit var config: PNConfiguration + + override fun onBefore() { + config = PNConfiguration().apply { + subscribeKey = "demo" + publishKey = "demo" + } + } + + override fun onAfter() { + pubnub.destroy() + } + + @Test + fun testCreateSuccess() { + pubnub = PubNub(config) + assertEquals(true, pubnub.configuration.secure) + assertEquals("https://ps.pndsn.com", pubnub.baseUrl()) + } + + @Test + @Throws(PubNubException::class) + fun testEncryptCustomKey() { + pubnub = PubNub(config) + assertEquals("iALQtn3PfIXe74CT/wrS7g==", pubnub.encrypt("test1", "cipherKey").trim()) + } + + @Test + @Throws(PubNubException::class) + fun testEncryptConfigurationKey() { + config.cipherKey = "cipherKey" + pubnub = PubNub(config) + assertEquals("iALQtn3PfIXe74CT/wrS7g==", pubnub.encrypt("test1").trim()) + } + + @Test + @Throws(PubNubException::class) + fun testDecryptCustomKey() { + pubnub = PubNub(config) + assertEquals("test1", pubnub.decrypt("iALQtn3PfIXe74CT/wrS7g==", "cipherKey").trim()) + } + + @Test + @Throws(PubNubException::class) + fun testDecryptConfigurationKey() { + config.cipherKey = "cipherKey" + pubnub = PubNub(config) + assertEquals("test1", pubnub.decrypt("iALQtn3PfIXe74CT/wrS7g==").trim()) + } + + @Test + fun testconfig() { + config.subscribeTimeout = 3000 + config.connectTimeout = 4000 + config.nonSubscribeRequestTimeout = 5000 + config.reconnectionPolicy = PNReconnectionPolicy.NONE + pubnub = PubNub(config) + assertEquals("https://ps.pndsn.com", pubnub.baseUrl()) + assertEquals(3000, config.subscribeTimeout) + assertEquals(4000, config.connectTimeout) + assertEquals(5000, config.nonSubscribeRequestTimeout) + } + + @Test + fun getVersionAndTimeStamp() { + pubnub = PubNub(config) + val version = pubnub.version + val timeStamp = pubnub.timestamp() + assertEquals("4.0.0-dev", version) + assertTrue(timeStamp > 0) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/DeleteMessagesEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/DeleteMessagesEndpointTest.kt new file mode 100644 index 000000000..4dc7adb17 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/DeleteMessagesEndpointTest.kt @@ -0,0 +1,136 @@ +package com.pubnub.api.legacy.endpoints + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class DeleteMessagesEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + delete(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "" + } + """.trimIndent() + ) + ) + ) + + pubnub.deleteMessages().apply { + channels = listOf("mychannel,my_channel") + }.sync()!! + } + + @Test + fun testSyncAuthSuccess() { + stubFor( + delete(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.deleteMessages().apply { + channels = listOf("mychannel,my_channel") + }.sync()!! + + val requests = findAll(deleteRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testFailure() { + stubFor( + delete(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 403, + "error": false, + "error_message": "wut" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.deleteMessages().apply { + channels = listOf("mychannel,my_channel") + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + @Test + fun testAsyncSuccess() { + stubFor( + delete(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "" + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.deleteMessages().apply { + channels = listOf("mychannel,my_channel") + }.async { result, status -> + result!! + assertFalse(status.error) + assertEquals(PNOperationType.PNDeleteMessagesOperation, status.operation) + success.set(true) + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(success) + } + + @Test + fun testMissingChannel() { + try { + pubnub.deleteMessages().sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/EndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/EndpointTest.kt new file mode 100644 index 000000000..d0ae912ba --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/EndpointTest.kt @@ -0,0 +1,309 @@ +package com.pubnub.api.legacy.endpoints + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.matching.UrlPattern +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import okhttp3.Request +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +class EndpointTest : BaseTest() { + + @Test + fun testDefaultInstanceParamSetting() { + assertTrue(pubnub.configuration.includeRequestIdentifier) + assertFalse(pubnub.configuration.includeInstanceIdentifier) + } + + // todo test other params + + @Test + fun testBaseParamsIncludeInstanceId() { + pubnub.configuration.includeInstanceIdentifier = true + + fakeEndpoint { + assertTrue(it.containsKey("instanceid")) + }.sync() + } + + @Test + fun testBaseParamsNoIncludeInstanceId() { + fakeEndpoint { + assertEquals("myUUID", it["uuid"]) + assertFalse(it.containsKey("instanceid")) + }.sync() + } + + @Test + fun testBaseParamsIncludeRequestId() { + fakeEndpoint { + assertEquals("myUUID", it["uuid"]) + assertTrue(it.containsKey("requestid")) + }.sync() + } + + @Test + fun testBaseParamsNoIncludeRequestId() { + pubnub.configuration.includeRequestIdentifier = false + + fakeEndpoint { + assertEquals("myUUID", it["uuid"]) + assertFalse(it.containsKey("requestid")) + }.sync() + } + + @Test + fun testBaseParamsPersistentRequestId() { + pubnub.configuration.includeInstanceIdentifier = true + + val instanceId1 = AtomicReference() + val instanceId2 = AtomicReference() + + fakeEndpoint { + instanceId1.set(it["instanceid"]) + }.sync() + + fakeEndpoint { + instanceId2.set(it["instanceid"]) + }.sync() + + assertEquals(pubnub.instanceId, instanceId1.get()) + assertEquals(instanceId1.get(), instanceId2.get()) + } + + @Test + fun testUuid() { + val expectedUuid = UUID.randomUUID().toString() + pubnub.configuration.uuid = expectedUuid + + fakeEndpoint { + assertEquals(expectedUuid, it["uuid"]) + }.sync() + } + + @Test + fun testQueryParam() { + fakeEndpoint { + assertEquals("sf", it["city"]) + assertEquals(pubnub.configuration.uuid, it["uuid"]) + assertEquals(4, it.size) + assertTrue(it.contains("pnsdk")) + assertTrue(it.contains("requestid")) + assertTrue(it.contains("uuid")) + }.apply { + queryParam = mapOf( + "city" to "sf", + "uuid" to "overwritten" + ) + }.sync() + } + + @Test + fun testQueryParamEmpty() { + fakeEndpoint { + assertEquals(3, it.size) + assertTrue(it.contains("pnsdk")) + assertTrue(it.contains("requestid")) + assertTrue(it.contains("uuid")) + }.apply { + queryParam = emptyMap() + }.sync() + } + + @Test + fun testQueryParamMissing() { + fakeEndpoint { + assertEquals(3, it.size) + assertTrue(it.contains("pnsdk")) + assertTrue(it.contains("requestid")) + assertTrue(it.contains("uuid")) + }.sync() + } + + @Test + fun testErrorAffectedChannelsAndChannelGroups() { + stubFor( + any(UrlPattern.ANY) + .willReturn( + aResponse() + .withStatus(400) + .withBody( + JsonObject().apply { + add("payload", JsonObject().apply { + add("channels", JsonArray().apply { add("ch1");add("ch2") }) + add("channel-groups", JsonArray().apply { add("cg1") }) + }) + }.toString() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.time() + .async { result, status -> + assertEquals(listOf("ch1", "ch2"), status.affectedChannels) + assertEquals(listOf("cg1"), status.affectedChannelGroups) + success.set(true) + } + + success.listen() + } + + @Test + fun testNoAffectedChannelsAndChannelGroups() { + stubFor( + any(UrlPattern.ANY) + .willReturn( + aResponse() + .withStatus(400) + .withBody("""{}""") + ) + ) + + val success = AtomicBoolean() + + pubnub.time() + .async { _, status -> + assertTrue(status.affectedChannels.isEmpty()) + assertTrue(status.affectedChannelGroups.isEmpty()) + success.set(true) + } + + success.listen() + } + + @Test + fun testNoSecretKeySignatureParam() { + pubnub.configuration.secretKey = "" + + stubFor( + any(UrlPattern.ANY).willReturn( + aResponse().withBody("""[100]""") + ) + ) + + val success = AtomicBoolean() + + pubnub.time() + .async { _, status -> + assertNull(status.param("signature")) + success.set(true) + } + + success.listen() + } + + @Test + fun testSecretKeySignatureParam() { + pubnub.configuration.secretKey = "mySecretKey" + + stubFor( + any(UrlPattern.ANY).willReturn( + aResponse().withBody("""[100]""") + ) + ) + + val success = AtomicBoolean() + + pubnub.time() + .async { _, status -> + assertNotNull(status.param("signature")) + success.set(true) + } + + success.listen() + } + + + @Test + fun testUnauthorized() { + stubFor( + any(UrlPattern.ANY) + .willReturn( + forbidden() + ) + ) + + val success = AtomicBoolean() + + pubnub.time() + .async { _, status -> + assertTrue(status.error) + assertEquals(PNStatusCategory.PNAccessDeniedCategory, status.category) + success.set(true) + } + + success.listen() + } + + @Test + fun testDefaultTimeoutValues() { + val p = PubNub(PNConfiguration()) + assertEquals(300, p.configuration.presenceTimeout) + assertEquals(0, p.configuration.heartbeatInterval) + } + + @Test + fun testCustomTimeoutValues1() { + val p = PubNub(PNConfiguration()) + p.configuration.presenceTimeout = 100 + assertEquals(100, p.configuration.presenceTimeout) + assertEquals(49, p.configuration.heartbeatInterval) + } + + @Test + fun testCustomTimeoutValues2() { + val p = PubNub(PNConfiguration()) + p.configuration.heartbeatInterval = 100 + assertEquals(300, p.configuration.presenceTimeout) + assertEquals(100, p.configuration.heartbeatInterval) + } + + @Test + fun testCustomTimeoutValues3() { + val p = PubNub(PNConfiguration()) + p.configuration.heartbeatInterval = 40 + p.configuration.presenceTimeout = 50 + assertEquals(50, p.configuration.presenceTimeout) + assertEquals(24, p.configuration.heartbeatInterval) + } + + + private fun fakeEndpoint( + paramsCondition: (map: HashMap) -> Unit + ) = object : Endpoint(pubnub) { + + override fun doWork(queryParams: HashMap): Call { + paramsCondition.invoke(queryParams) + return fakeCall() + } + + override fun createResponse(input: Response) = this + override fun operationType() = PNOperationType.PNSubscribeOperation + override fun isSubKeyRequired() = false + override fun isPubKeyRequired() = false + override fun isAuthRequired() = false + } + + private fun fakeCall() = object : Call { + override fun enqueue(callback: Callback) {} + override fun isExecuted() = false + override fun clone(): Call = this + override fun isCanceled() = false + override fun cancel() {} + override fun execute(): Response = Response.success(null) + override fun request() = Request.Builder().build() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/HeartbeatEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/HeartbeatEndpointTest.kt new file mode 100644 index 000000000..f03b85ce7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/HeartbeatEndpointTest.kt @@ -0,0 +1,249 @@ +package com.pubnub.api.legacy.endpoints + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.endpoints.presence.Heartbeat +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class HeartbeatEndpointTest : BaseTest() { + + @Test + fun testSuccessOneChannel() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + Heartbeat(pubnub, listOf("ch1")).sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + assertEquals("123", request.queryParameter("heartbeat").firstValue()) + } + + @Test + fun testSuccessManyChannels() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1,ch2/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + Heartbeat(pubnub, listOf("ch1", "ch2")).sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + assertEquals("123", request.queryParameter("heartbeat").firstValue()) + } + + @Test + fun testSuccessOneChannelGroup() { + pubnub.configuration.presenceTimeout = 123 + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + Heartbeat(pubnub, channelGroups = listOf("cg1")).sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + assertEquals("cg1", request.queryParameter("channel-group").firstValue()) + assertEquals("123", request.queryParameter("heartbeat").firstValue()) + } + + @Test + fun testSuccessManyChannelGroups() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + Heartbeat(pubnub, channelGroups = listOf("cg1", "cg2")).sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + assertEquals("cg1,cg2", request.queryParameter("channel-group").firstValue()) + assertEquals("123", request.queryParameter("heartbeat").firstValue()) + } + + @Test + fun testMissingChannelAndGroupSync() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + Heartbeat(pubnub).sync()!! + failTest() + } catch (e: Exception) { + assertPnException( + PubNubError.CHANNEL_AND_GROUP_MISSING, + e + ) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + Heartbeat(pubnub, listOf("ch1")).sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testBlankSubKeySync() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + pubnub.configuration.subscribeKey = " " + + try { + Heartbeat(pubnub, listOf("ch1")).sync()!! + failTest() + } catch (e: Exception) { + assertPnException( + PubNubError.SUBSCRIBE_KEY_MISSING, + e + ) + } + } + + @Test + fun testEmptySubKeySync() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + pubnub.configuration.subscribeKey = "" + + try { + Heartbeat(pubnub, listOf("ch1")).sync()!! + failTest() + } catch (e: Exception) { + assertPnException( + PubNubError.SUBSCRIBE_KEY_MISSING, + e + ) + } + + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/access/GrantEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/access/GrantEndpointTest.kt new file mode 100644 index 000000000..eb32a32c7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/access/GrantEndpointTest.kt @@ -0,0 +1,1736 @@ +package com.pubnub.api.legacy.endpoints.access + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerKeyData +import org.awaitility.Awaitility +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class GrantEndpointTest : BaseTest() { + + override fun onBefore() { + pubnub.configuration.secretKey = "secretKey" + pubnub.configuration.includeInstanceIdentifier = true + pubnub.configuration.includeRequestIdentifier = true + } + + @Test + fun noGroupsOneChannelOneKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) // todo replace with error + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelTwoKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channels = listOf("ch1") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsTwoChannelOneKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1", "ch2") + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(1, result.channels["ch2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsTwoChannelTwoKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = (listOf("key1", "key2")) + channels = (listOf("ch1", "ch2")) + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(2, result.channels["ch2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key2"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupNoChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel-groups": "cg1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(0, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupNoChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel-groups": "cg1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(0, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupOneChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + }, + "channel-groups": "cg1" + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupOneChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + }, + "channel-groups": "cg1" + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channels = listOf("ch1") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupTwoChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + }, + "channel-groups": "cg1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun oneGroupTwoChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("channel-group", matching("cg1")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + }, + "channel-groups": "cg1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1") + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(1, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key2"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + + @Test + fun twoGroupNoChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel-group", matching("cg1,cg2")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(0, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(1, result.channelGroups["cg2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun twoGroupNoChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel-group", matching("cg1,cg2")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(0, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key2"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun twoGroupOneChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("channel-group", matching("cg1,cg2")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + }, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(1, result.channelGroups["cg2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun twoGroupOneChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("channel-group", matching("cg1,cg2")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + }, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channels = listOf("ch1") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(2, result.channelGroups["cg2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key2"]!!.javaClass) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun twoGroupTwoChannelOneKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + }, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(1, result.channelGroups["cg1"]!!.size) + assertEquals(1, result.channelGroups["cg2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(1, result.channels["ch2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun twoGroupTwoChannelTwoKey() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1,ch2")) + .withQueryParam("channel-group", matching("cg1,cg2")) + .withQueryParam("auth", matching("key1,key2")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "channel-group+auth", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channels": { + "ch1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "ch2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + }, + "channel-groups": { + "cg1": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "cg2": { + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + }, + "key2": { + "r": 0, + "w": 0, + "m": 0 + } + } + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1", "key2") + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + assertEquals(2, result.channels.size) + assertEquals(2, result.channelGroups.size) + assertEquals(2, result.channelGroups["cg1"]!!.size) + assertEquals(2, result.channelGroups["cg2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channelGroups["cg2"]!!["key2"]!!.javaClass) + assertEquals(2, result.channels["ch1"]!!.size) + assertEquals(2, result.channels["ch2"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key2"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key1"]!!.javaClass) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch2"]!!["key2"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelOneKeyTTLTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .withQueryParam("ttl", matching("1334")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + ttl = 1334 + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelOneReadKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("1")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + read = true + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelOneWriteKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("1")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + write = true + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelOneDeleteKeyTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .withQueryParam("d", matching("1")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + delete = true + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun noGroupsOneChannelOneKeyManageTest() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("1")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + manage = true + }.sync()!! + + assertEquals(1, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals(1, result.channels["ch1"]!!.size) + assertEquals(PNAccessManagerKeyData::class.java, result.channels["ch1"]!!["key1"]!!.javaClass) + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + pubnub.configuration.authKey = "myKey" + + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/v2/auth/grant/sub-key/mySubscribeKey.*"))) + assertEquals(1, requests.size) + + decomposeAndVerifySignature(pubnub.configuration, requests[0]) + } + + @Test + fun testOperationTypeSuccessAsync() { + val atomic = AtomicBoolean(false) + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.async { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAccessManagerGrant, status.operation) + atomic.set(true) + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(atomic) + } + + @Test + fun testBlankSecretKey() { + pubnub.configuration.secretKey = " " + try { + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SECRET_KEY_MISSING, e) + } + } + + @Test + fun testEmptySecretKey() { + pubnub.configuration.secretKey = "" + try { + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SECRET_KEY_MISSING, e) + } + } + + @Test + fun testBlankSubscribeKey() { + pubnub.configuration.subscribeKey = " " + try { + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptySubscribeKey() { + pubnub.configuration.subscribeKey = "" + try { + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testMissingChannelsAndChannelGroup() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withStatus(200).withBody( + """ + { + "message": "Success", + "payload": { + "level": "subkey", + "subscribe_key": "mySubscribeKey", + "ttl": 1440, + "r": 0, + "w": 1, + "m": 0, + "d": 0 + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + try { + val grantResult = pubnub.grant().sync()!! + assertEquals("subkey", grantResult.level) + assertEquals(1440, grantResult.ttl) + assertEquals(0, grantResult.channels.size) + assertEquals(0, grantResult.channelGroups.size) + } catch (e: PubNubException) { + failTest() + } + } + + @Test + fun testNullPayload() { + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("auth", matching("key1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """{"message":"Success","service":"Access Manager","status":200}""" + ) + ) + ) + + try { + pubnub.grant().apply { + authKeys = listOf("key1") + channels = listOf("ch1") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNullAuthKeyAsync() { + val atomic = AtomicBoolean(false) + stubFor( + get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("channel", matching("ch1")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("r", matching("0")) + .withQueryParam("w", matching("0")) + .withQueryParam("m", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "message": "Success", + "payload": { + "level": "user", + "subscribe_key": "sub-c-82ab2196-b64f-11e5-8622-0619f8945a4f", + "ttl": 1, + "channel": "ch1", + "auths": { + "key1": { + "r": 0, + "w": 0, + "m": 0 + } + } + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + ) + ) + ) + + pubnub.grant().apply { + channels = listOf("ch1") + }.async { result, status -> + if (status.operation == PNOperationType.PNAccessManagerGrant && !status.error) { + atomic.set(true) + } + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(atomic) + } + +} diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AddChannelChannelGroupEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AddChannelChannelGroupEndpointTest.kt new file mode 100644 index 000000000..3fa628497 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AddChannelChannelGroupEndpointTest.kt @@ -0,0 +1,208 @@ +package com.pubnub.api.legacy.endpoints.channel_groups + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class AddChannelChannelGroupEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson()) + ) + + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.sync()!! + } + + @Test + fun testSyncGroupMissing() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson()) + ) + + try { + pubnub.addChannelsToChannelGroup().apply { + channels = listOf("ch1", "ch2") + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testSyncGroupIsEmpty() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn(emptyJson()) + ) + + try { + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "" + channels = listOf("ch1", "ch2") + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testSyncChannelMissing() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn(emptyJson()) + ) + + try { + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + }.sync() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testIsSubRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson()) + ) + + pubnub.configuration.subscribeKey = " " + + try { + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson()) + ) + pubnub.configuration.authKey = "myKey" + + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson()) + ) + + val atomic = AtomicInteger(0) + + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAddChannelsToGroupOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + assertTrue(status.affectedChannels == listOf("ch1", "ch2")) + assertTrue(status.affectedChannelGroups == listOf("groupA")) + atomic.incrementAndGet() + } + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testErrorBodyForbidden() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .withQueryParam("add", matching("ch1,ch2")) + .willReturn(emptyJson().withStatus(403)) + ) + + val atomic = AtomicInteger(0) + + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.async { _, status -> + assertTrue(status.error) + assertEquals(PNOperationType.PNAddChannelsToGroupOperation, status.operation) + assertEquals(PNStatusCategory.PNAccessDeniedCategory, status.category) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(15, TimeUnit.SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn(emptyJson()) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.addChannelsToChannelGroup().apply { + channelGroup = "groupA" + channels = listOf("ch1", "ch2") + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAddChannelsToGroupOperation, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_cg", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AllChannelsChannelGroupEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AllChannelsChannelGroupEndpointTest.kt new file mode 100644 index 000000000..773ab212a --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/AllChannelsChannelGroupEndpointTest.kt @@ -0,0 +1,214 @@ +package com.pubnub.api.legacy.endpoints.channel_groups + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class AllChannelsChannelGroupEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.listChannelsForChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + + assertThat(response.channels, Matchers.contains("a", "b")) + } + + @Test + fun testSyncMissingGroup() { + try { + pubnub.listChannelsForChannelGroup().apply { + + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testSyncEmptyGroup() { + try { + pubnub.listChannelsForChannelGroup().apply { + channelGroup = " " + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + pubnub.configuration.authKey = "myKey" + + pubnub.listChannelsForChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.listChannelsForChannelGroup().apply { + channelGroup = "groupA" + }.async { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNChannelsForGroupOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + assertTrue(status.affectedChannels.isEmpty()) + assertEquals(listOf("groupA"), status.affectedChannelGroups) + assertEquals(listOf("a", "b"), result!!.channels) + atomic.incrementAndGet() + } + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testIsSubRequiredSuccessSync() { + pubnub.configuration.subscribeKey = " " + try { + pubnub.listChannelsForChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.listChannelsForChannelGroup().apply { + channelGroup = "groupA" + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNChannelsForGroupOperation, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_cg", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/DeleteChannelGroupEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/DeleteChannelGroupEndpointTest.kt new file mode 100644 index 000000000..1ab001fbb --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/DeleteChannelGroupEndpointTest.kt @@ -0,0 +1,164 @@ +package com.pubnub.api.legacy.endpoints.channel_groups + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class DeleteChannelGroupEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA/remove")) + .willReturn( + aResponse().withBody( + """{"status":200,"message":"OK","payload":{},"service":"ChannelGroups"}""" + ) + ) + ) + + pubnub.deleteChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + } + + @Test + fun testSyncMissingGroup() { + try { + pubnub.deleteChannelGroup().apply { + + }.sync()!! + } catch (e: PubNubException) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testSyncEmptyGroup() { + stubFor(any(anyUrl()).willReturn(aResponse())) + + try { + pubnub.deleteChannelGroup().apply { + channelGroup = " " + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA/remove")) + .willReturn( + aResponse().withBody( + """{"status": 200, "message": "OK", "payload": {}, "service": "ChannelGroups"}""" + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.deleteChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + + val requests = + findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testMalformedResponse() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA/remove")) + .willReturn(noContent()) + ) + + pubnub.deleteChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA/remove")) + .willReturn( + aResponse().withBody( + """{"status": 200, "message": "OK", "payload": {}, "service": "ChannelGroups"}""" + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.deleteChannelGroup().apply { + channelGroup = "groupA" + }.async { _, status -> + Assertions.assertFalse(status.error) + assertEquals(PNOperationType.PNRemoveGroupOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertTrue(status.affectedChannels == emptyList()) + Assertions.assertTrue(status.affectedChannelGroups == listOf("groupA")) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA/remove")) + .willReturn( + aResponse().withBody( + """{"status": 200, "message": "OK", "payload": {}, "service": "ChannelGroups"}""" + ) + ) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.deleteChannelGroup().apply { + channelGroup = "groupA" + }.async { _, status -> + Assertions.assertFalse(status.error) + assertEquals(PNOperationType.PNRemoveGroupOperation, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_cg", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + Assertions.assertFalse(status.error) + Assertions.assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/ListAllChannelGroupEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/ListAllChannelGroupEndpointTest.kt new file mode 100644 index 000000000..de770c55c --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/ListAllChannelGroupEndpointTest.kt @@ -0,0 +1,158 @@ +package com.pubnub.api.legacy.endpoints.channel_groups + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class ListAllChannelGroupEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "groups": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.listAllChannelGroups().sync()!! + assertThat(response.groups, Matchers.contains("a", "b")) // todo matchers + } + + @Test + fun testNullPayload() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + .willReturn( + aResponse().withBody(""" + { + "status": 200, + "message": "OK", + "service": "ChannelGroups" + } + """.trimIndent()) + ) + ) + + try { + pubnub.listAllChannelGroups().sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNullBody() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + .willReturn(aResponse()) + ) + + try { + pubnub.listAllChannelGroups().sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "groups": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.listAllChannelGroups().sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "groups": [ + "a", + "b" + ] + }, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.listAllChannelGroups() + .async { _, status -> + Assertions.assertFalse(status.error) + assertEquals(PNOperationType.PNChannelGroupsOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertTrue(status.affectedChannels.isEmpty()) + Assertions.assertTrue(status.affectedChannelGroups.isEmpty()) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/RemoveChannelChannelGroupEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/RemoveChannelChannelGroupEndpointTest.kt new file mode 100644 index 000000000..40f2f67c7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/channel_groups/RemoveChannelChannelGroupEndpointTest.kt @@ -0,0 +1,162 @@ +package com.pubnub.api.legacy.endpoints.channel_groups + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class RemoveChannelChannelGroupEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": {}, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = "groupA" + channels = arrayListOf("ch1", "ch2") + }.sync()!! + } + + @Test + fun testSyncMissingGroup() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": {}, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.removeChannelsFromChannelGroup().apply { + channels = arrayListOf("ch1", "ch2") + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.GROUP_MISSING, e) + } + } + + @Test + fun testSyncMissingChannel() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": {}, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = "groupA" + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": {}, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = "groupA" + channels = arrayListOf("ch1", "ch2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/groupA")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": {}, + "service": "ChannelGroups" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = "groupA" + channels = arrayListOf("ch1", "ch2") + }.async { _, status -> + Assertions.assertFalse(status.error) + assertEquals(PNOperationType.PNRemoveChannelsFromGroupOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertTrue(status.affectedChannels == listOf("ch1", "ch2")) + Assertions.assertTrue(status.affectedChannelGroups == listOf("groupA")) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/FetchMessagesEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/FetchMessagesEndpointTest.kt new file mode 100644 index 000000000..138950ff5 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/FetchMessagesEndpointTest.kt @@ -0,0 +1,156 @@ +package com.pubnub.api.legacy.endpoints.history + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class FetchMessagesEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": [ + { + "message": "hihi", + "timetoken": "14698320467224036" + }, + { + "message": "Hey", + "timetoken": "14698320468265639" + } + ], + "mychannel": [ + { + "message": "sample message", + "timetoken": "14369823849575729" + } + ] + } + } + """.trimIndent() + ) + ) + ) + + + val response = pubnub.fetchMessages().apply { + channels = listOf("mychannel", "my_channel") + maximumPerChannel = 25 + }.sync()!! + + assertEquals(response.channels.size, 2) + assertTrue(response.channels.containsKey("mychannel")) + assertTrue(response.channels.containsKey("my_channel")) + assertEquals(response.channels["mychannel"]!!.size, 1) + assertEquals(response.channels["my_channel"]!!.size, 2) + } + + @Test + fun testSyncAuthSuccess() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": [ + { + "message": "hihi", + "timetoken": "14698320467224036" + }, + { + "message": "Hey", + "timetoken": "14698320468265639" + } + ], + "mychannel": [ + { + "message": "sample message", + "timetoken": "14369823849575729" + } + ] + } + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + val response = pubnub.fetchMessages().apply { + channels = listOf("mychannel", "my_channel") + maximumPerChannel = 25 + }.sync()!! + + assertEquals(response.channels.size, 2) + assertTrue(response.channels.containsKey("mychannel")) + assertTrue(response.channels.containsKey("my_channel")) + assertEquals(response.channels["mychannel"]!!.size, 1) + assertEquals(response.channels["my_channel"]!!.size, 2) + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + assertEquals(1, requests.size) + } + + @Test + fun testSyncEncryptedSuccess() { + pubnub.configuration.cipherKey = "testCipher" + + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/mychannel,my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": [ + { + "message": "jC/yJ2y99BeYFYMQ7c53pg==", + "timetoken": "14797423056306675" + } + ], + "mychannel": [ + { + "message": "jC/yJ2y99BeYFYMQ7c53pg==", + "timetoken": "14797423056306675" + } + ] + } + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.fetchMessages().apply { + channels = listOf("mychannel", "my_channel") + maximumPerChannel = 25 + }.sync()!! + + assertEquals(response.channels.size, 2) + assertTrue(response.channels.containsKey("mychannel")) + assertTrue(response.channels.containsKey("my_channel")) + assertEquals(response.channels["mychannel"]!!.size, 1) + assertEquals(response.channels["my_channel"]!!.size, 1) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/HistoryEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/HistoryEndpointTest.kt new file mode 100644 index 000000000..d583267d3 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/HistoryEndpointTest.kt @@ -0,0 +1,534 @@ +package com.pubnub.api.legacy.endpoints.history + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class HistoryEndpointTest : BaseTest() { + + @Test + fun testSyncDisabled() { + val payload = """ + [ + "Use of the history API requires the Storage & Playback which is not enabled for this subscribe key. Login to your PubNub Dashboard Account and enable Storage & Playback. Contact support@pubnub.com if you require further assistance.", + 0, + 0 + ] + """.trimIndent() + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(payload)) + ) + + try { + pubnub.history().apply { + channel = "niceChannel" + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.HTTP_ERROR, e) + assertEquals("History is disabled", e.errorMessage) + } + } + + @Test + fun testSyncWithTokensDisabled() { + val payload = """ + [ + "Use of the history API requires the Storage & Playback which is not enabled for this subscribe key. Login to your PubNub Dashboard Account and enable Storage & Playback. Contact support@pubnub.com if you require further assistance.", + 0, + 0 + ] + """.trimIndent() + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(payload)) + ) + + try { + pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = true + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.HTTP_ERROR, e) + assertEquals("History is disabled", e.errorMessage) + } + } + + @Test + fun testSyncSuccess() { + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyEnvelope1 = mapOf( + "timetoken" to 1111, + "message" to historyItem1 + ) + + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + val historyEnvelope2 = mapOf( + "timetoken" to 2222, + "message" to historyItem2 + ) + + val historyItems = listOf( + historyEnvelope1, + historyEnvelope2 + ) + val testArray = listOf( + historyItems, + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + val result = pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = true + }.sync()!! + + with(result) { + assertEquals(1234, startTimetoken) + assertEquals(4321, endTimetoken) + + assertEquals(2, messages.size) + + assertEquals(1111, messages[0].timetoken) + assertEquals(11, messages[0].entry.asJsonObject["a"].asInt) + assertEquals(22, messages[0].entry.asJsonObject["b"].asInt) + + assertEquals(2222, messages[1].timetoken) + assertEquals(33, messages[1].entry.asJsonObject["a"].asInt) + assertEquals(44, messages[1].entry.asJsonObject["b"].asInt) + } + } + + @Test + fun testSyncAuthSuccess() { + pubnub.configuration.authKey = "authKey" + + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyEnvelope1 = mapOf( + "timetoken" to 1111, + "message" to historyItem1 + ) + + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + val historyEnvelope2 = mapOf( + "timetoken" to 2222, + "message" to historyItem2 + ) + + val historyItems = listOf( + historyEnvelope1, + historyEnvelope2 + ) + val testArray = listOf( + historyItems, + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = true + }.sync()!! + + val requests = findAll(getRequestedFor(anyUrl())) + assertEquals(1, requests.size) + assertEquals("authKey", requests.first().queryParameter("auth").firstValue()) + } + + @Test + fun testSyncEncryptedSuccess() { + pubnub.configuration.cipherKey = "testCipher" + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn( + aResponse().withBody( + """ + [ + [ + "EGwV+Ti43wh2TprPIq7o0KMuW5j6B3yWy352ucWIOmU=\n", + "EGwV+Ti43wh2TprPIq7o0KMuW5j6B3yWy352ucWIOmU=\n", + "EGwV+Ti43wh2TprPIq7o0KMuW5j6B3yWy352ucWIOmU=\n" + ], + 14606134331557852, + 14606134485013970 + ] + """.trimIndent() + ) + ) + ) + + val result = pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = false + }.sync()!! + + with(result) { + assertEquals(14606134331557852, startTimetoken) + assertEquals(14606134485013970, endTimetoken) + + assertEquals(3, messages.size) + + assertNull(messages[0].timetoken) + + assertEquals("m1", messages[0].entry.asJsonArray[0].asString) + assertEquals("m2", messages[0].entry.asJsonArray[1].asString) + assertEquals("m3", messages[0].entry.asJsonArray[2].asString) + + assertEquals("m1", messages[1].entry.asJsonArray[0].asString) + assertEquals("m2", messages[1].entry.asJsonArray[1].asString) + assertEquals("m3", messages[1].entry.asJsonArray[2].asString) + + assertEquals("m1", messages[2].entry.asJsonArray[0].asString) + assertEquals("m2", messages[2].entry.asJsonArray[1].asString) + assertEquals("m3", messages[2].entry.asJsonArray[2].asString) + } + } + + @Test + fun testSyncEncryptedWithPNOtherSuccess() { + pubnub.configuration.cipherKey = "hello" + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn( + aResponse().withBody( + """ + [ + [ + { + "pn_other": "6QoqmS9CnB3W9+I4mhmL7w==" + } + ], + 14606134331557852, + 14606134485013970 + ] + """.trimIndent() + ) + ) + ) + + + val result = pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = false + }.sync()!! + + with(result) { + assertEquals(14606134331557852, startTimetoken) + assertEquals(14606134485013970, endTimetoken) + + assertEquals(1, messages.size) + + assertNull(messages[0].timetoken) + assertEquals("hey", messages[0].entry.asJsonObject.get("pn_other").asJsonObject.get("text").asString) + } + } + + @Test + fun testSyncSuccessWithoutTimeToken() { + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + + val testArray = listOf( + listOf( + historyItem1, + historyItem2 + ), + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + val result = pubnub.history().apply { + channel = "niceChannel" + }.sync()!! + + with(result) { + assertEquals(1234, startTimetoken) + assertEquals(4321, endTimetoken) + + assertEquals(2, messages.size) + + assertNull(messages[0].timetoken) + assertEquals(11, messages[0].entry.asJsonObject["a"].asInt) + assertEquals(22, messages[0].entry.asJsonObject["b"].asInt) + + assertNull(messages[1].timetoken) + assertEquals(33, messages[1].entry.asJsonObject["a"].asInt) + assertEquals(44, messages[1].entry.asJsonObject["b"].asInt) + } + } + + @Test + fun testMissingChannel() { + try { + pubnub.history().apply { + + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testChannelIsEmpty() { + try { + pubnub.history().apply { + channel = "" + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testChannelIsBlank() { + try { + pubnub.history().apply { + channel = " " + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testOperationTypeSuccessAsync() { + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyEnvelope1 = mapOf( + "timetoken" to 1111, + "message" to historyItem1 + ) + + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + val historyEnvelope2 = mapOf( + "timetoken" to 2222, + "message" to historyItem2 + ) + + val historyItems = listOf( + historyEnvelope1, + historyEnvelope2 + ) + val testArray = listOf( + historyItems, + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + val success = AtomicBoolean() + + pubnub.history().apply { + channel = "niceChannel" + includeTimetoken = true + }.async { result, status -> + + assertEquals(PNOperationType.PNHistoryOperation, status.operation) + + with(result!!) { + assertEquals(1234, startTimetoken) + assertEquals(4321, endTimetoken) + + assertEquals(2, messages.size) + + assertEquals(1111, messages[0].timetoken) + assertEquals(11, messages[0].entry.asJsonObject["a"].asInt) + assertEquals(22, messages[0].entry.asJsonObject["b"].asInt) + + assertEquals(2222, messages[1].timetoken) + assertEquals(33, messages[1].entry.asJsonObject["a"].asInt) + assertEquals(44, messages[1].entry.asJsonObject["b"].asInt) + } + + success.set(true) + } + + success.listen() + } + + @Test + fun testSyncCountReverseStartEndSuccess() { + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyEnvelope1 = mapOf( + "timetoken" to 1111, + "message" to historyItem1 + ) + + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + val historyEnvelope2 = mapOf( + "timetoken" to 2222, + "message" to historyItem2 + ) + + val historyItems = listOf( + historyEnvelope1, + historyEnvelope2 + ) + val testArray = listOf( + historyItems, + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + val result = pubnub.history().apply { + channel = "niceChannel" + count = 5 + reverse = true + start = 1L + end = 2L + includeTimetoken = true + }.sync()!! + + val requests = findAll( + getRequestedFor( + urlMatching + ("/v2/history/sub-key/mySubscribeKey/channel/niceChannel.*") + ) + ) + + assertEquals(1, requests.size) + + assertEquals("true", requests.first().queryParameter("reverse").firstValue()) + assertEquals("5", requests.first().queryParameter("count").firstValue()) + assertEquals("1", requests.first().queryParameter("start").firstValue()) + assertEquals("2", requests.first().queryParameter("end").firstValue()) + assertEquals("true", requests.first().queryParameter("include_token").firstValue()) + + with(result) { + assertEquals(1234, startTimetoken) + assertEquals(4321, endTimetoken) + + assertEquals(2, messages.size) + + assertEquals(1111, messages[0].timetoken) + assertEquals(11, messages[0].entry.asJsonObject["a"].asInt) + assertEquals(22, messages[0].entry.asJsonObject["b"].asInt) + + assertEquals(2222, messages[1].timetoken) + assertEquals(33, messages[1].entry.asJsonObject["a"].asInt) + assertEquals(44, messages[1].entry.asJsonObject["b"].asInt) + } + + } + + @Test + fun testSyncProcessMessageError() { + val historyItem1 = mapOf( + "a" to 11, + "b" to 22 + ) + val historyEnvelope1 = mapOf( + "timetoken" to 1111, + "message" to historyItem1 + ) + + val historyItem2 = mapOf( + "a" to 33, + "b" to 44 + ) + val historyEnvelope2 = mapOf( + "timetoken" to 2222, + "message" to historyItem2 + ) + + val historyItems = listOf( + historyEnvelope1, + historyEnvelope2 + ) + val testArray = listOf( + historyItems, + 1234, + 4321 + ) + + stubFor( + get(urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/niceChannel")) + .willReturn(aResponse().withBody(pubnub.mapper.toJson(testArray))) + ) + + pubnub.configuration.cipherKey = "Test" + + try { + pubnub.history().apply { + channel = "niceChannel" + count = 5 + reverse = true + start = 1L + end = 2L + includeTimetoken = true + }.sync()!! + failTest() + } catch (e: UnsupportedOperationException) { + + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/MessageCountTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/MessageCountTest.kt new file mode 100644 index 000000000..7d243913d --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/history/MessageCountTest.kt @@ -0,0 +1,302 @@ +package com.pubnub.api.legacy.endpoints.history + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class MessageCountTest : BaseTest() { + + @Test + fun testSingleChannelWithSingleToken() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19 + } + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.messageCounts().apply { + channels = listOf("my_channel") + channelsTimetoken = listOf(10000L) + }.sync()!! + + assertEquals(response.channels.size, 1) + assertFalse(response.channels.containsKey("channel_does_not_exist")) + assertTrue(response.channels.containsKey("my_channel")) + for ((key, value) in response.channels) { + assertEquals("my_channel", key) + assertEquals(java.lang.Long.valueOf("19"), value) + } + } + + @Test + fun testSingleChannelWithMultipleTokens() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19 + } + } + """.trimIndent() + ) + ) + ) + var exception: PubNubException? = null + try { + pubnub.messageCounts().apply { + channels = listOf("my_channel") + channelsTimetoken = listOf(10000L, 20000L) + }.sync() + } catch (e: PubNubException) { + exception = e + } finally { + assertNotNull(exception) + assertEquals( + PubNubError.CHANNELS_TIMETOKEN_MISMATCH.message, + exception!!.pubnubError!!.message + ) + } + } + + @Test + @Throws(PubNubException::class) + fun testMultipleChannelsWithSingleToken() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel,new_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19, + "new_channel": 5 + } + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.messageCounts().apply { + channels = listOf("my_channel", "new_channel") + channelsTimetoken = listOf(10000L) + }.sync()!! + + assertEquals(response.channels.size, 2) + assertFalse(response.channels.containsKey("channel_does_not_exist")) + assertTrue(response.channels.containsKey("my_channel")) + assertTrue(response.channels.containsKey("new_channel")) + for ((key, value) in response.channels) { + if (key == "my_channel") { + assertEquals(java.lang.Long.valueOf("19"), value) + } else if (key == "new_channel") { + assertEquals(java.lang.Long.valueOf("5"), value) + } + } + } + + @Test + @Throws(PubNubException::class) + fun testMultipleChannelsWithMultipleTokens() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel,new_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19, + "new_channel": 5 + } + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.messageCounts().apply { + channels = listOf("my_channel", "new_channel") + channelsTimetoken = listOf(10000L, 20000L) + }.sync()!! + + assertEquals(response.channels.size, 2) + assertFalse(response.channels.containsKey("channel_does_not_exist")) + assertTrue(response.channels.containsKey("my_channel")) + assertTrue(response.channels.containsKey("new_channel")) + for ((key, value) in response.channels) { + if (key == "my_channel") { + assertEquals(java.lang.Long.valueOf("19"), value) + } else if (key == "new_channel") { + assertEquals(java.lang.Long.valueOf("5"), value) + } + } + } + + @Test + fun testWithoutTimeToken() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19 + } + } + """.trimIndent() + ) + ) + ) + var exception: PubNubException? = null + try { + pubnub.messageCounts().apply { + channels = listOf("my_channel") + }.sync() + } catch (ex: PubNubException) { + exception = ex + } finally { + assertNotNull(exception) + assertEquals( + PubNubError.TIMETOKEN_MISSING.message, + exception!!.pubnubError!!.message + ) + } + } + + @Test + fun testWithoutChannelsSingleToken() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19, + "new_channel": 5 + } + } + """.trimIndent() + ) + ) + ) + var exception: PubNubException? = null + try { + pubnub.messageCounts().apply { + channelsTimetoken = listOf(10000L) + }.sync() + } catch (ex: PubNubException) { + exception = ex + } finally { + assertNotNull(exception) + assertEquals( + PubNubError.CHANNEL_MISSING.message, + exception!!.pubnubError!!.message + ) + } + } + + @Test + fun testWithoutChannelsMultipleTokens() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19, + "new_channel": 5 + } + } + """.trimIndent() + ) + ) + ) + var exception: PubNubException? = null + try { + pubnub.messageCounts().apply { + channelsTimetoken = listOf(10000L, 20000L) + }.sync() + } catch (ex: PubNubException) { + exception = ex + } finally { + assertNotNull(exception) + assertEquals( + PubNubError.CHANNEL_MISSING.message, + exception!!.pubnubError!!.message + ) + } + } + + @Test + fun testChannelWithSingleEmptyToken() { + stubFor( + get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/my_channel")) + .willReturn( + aResponse().withBody(""" + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "my_channel": 19 + } + } + """.trimIndent()) + ) + ) + var exception: PubNubException? = null + try { + pubnub.messageCounts().apply { + channels = listOf("my_channel") + }.sync() + } catch (ex: PubNubException) { + exception = ex + } finally { + assertNotNull(exception) + assertEquals( + PubNubError.TIMETOKEN_MISSING.message, + exception!!.pubnubError!!.message + ) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/AddMessageActionEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/AddMessageActionEndpointTest.kt new file mode 100644 index 000000000..62ccdf504 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/AddMessageActionEndpointTest.kt @@ -0,0 +1,433 @@ +package com.pubnub.api.legacy.endpoints.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class AddMessageActionEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + + assertEquals(result.messageTimetoken, 123) + assertEquals(result.type, "emoji") + assertEquals(result.uuid, "someUuid") + assertEquals(result.value, "smiley") + assertEquals(result.actionTimetoken, 1000) + } + + @Test + fun testAsyncSuccess() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.async { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAddMessageAction, status.operation) + assertEquals(result!!.messageTimetoken, 123) + assertEquals(result.type, "emoji") + assertEquals(result.uuid, "someUuid") + assertEquals(result.value, "smiley") + assertEquals(result.actionTimetoken, 1000) + success.set(true) + } + + success.listen() + } + + @Test + fun testMalformedJson() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + + @Test + fun testEmptyBody() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn(emptyJson()) + ) + + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNoBody() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn(noContent()) + ) + + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNoData() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200 + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNullData() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": null + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNoChannel() { + try { + pubnub.addMessageAction().apply { + + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testBlankChannel() { + try { + pubnub.addMessageAction().apply { + channel = " " + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testNoMessageAction() { + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_MISSING, e) + } + } + + + @Test + fun testBlankType() { + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = " ", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_TYPE_MISSING, e) + } + } + + @Test + fun testBlankValue() { + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = " ", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_VALUE_MISSING, e) + } + } + + @Test + fun testInvalidSubKey() { + pubnub.configuration.subscribeKey = " " + try { + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = " ", + messageTimetoken = 123 + ) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testAuthKeyRequired() { + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.sync()!! + + val requests = findAll(postRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + post(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123")) + .withRequestBody(equalToJson("""{"type":"emoji","value":"smiley"}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.addMessageAction().apply { + channel = "coolChannel" + messageAction = PNMessageAction( + type = "emoji", + value = "smiley", + messageTimetoken = 123 + ) + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNAddMessageAction, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_msga", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/GetMessageActionEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/GetMessageActionEndpointTest.kt new file mode 100644 index 000000000..5fdd32599 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/GetMessageActionEndpointTest.kt @@ -0,0 +1,488 @@ +package com.pubnub.api.legacy.endpoints.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class GetMessageActionEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + + assertEquals(result.actions.size, 1) + assertEquals(result.actions[0].messageTimetoken, 123) + assertEquals(result.actions[0].type, "emoji") + assertEquals(result.actions[0].uuid, "someUuid") + assertEquals(result.actions[0].value, "smiley") + assertEquals(result.actions[0].actionTimetoken, 1000) + } + + @Test + fun testSyncSuccessMultipleActions() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody(""" + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "uuid_1", + "value": "❤️", + "actionTimetoken": "1000" + }, + { + "messageTimetoken": "456", + "type": "reaction", + "uuid": "uuid_2", + "value": "👍", + "actionTimetoken": "2000" + }, + { + "messageTimetoken": "789", + "type": "emoji", + "uuid": "uuid_2", + "value": "😄", + "actionTimetoken": "3000" + } + ] + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + + assertEquals(result.actions.size, 3) + + assertEquals(result.actions[0].messageTimetoken, 123) + assertEquals(result.actions[0].type, "emoji") + assertEquals(result.actions[0].uuid, "uuid_1") + assertEquals(result.actions[0].value, "❤️") + assertEquals(result.actions[0].actionTimetoken, 1000) + + assertEquals(result.actions[1].messageTimetoken, 456) + assertEquals(result.actions[1].type, "reaction") + assertEquals(result.actions[1].uuid, "uuid_2") + assertEquals(result.actions[1].value, "👍") + assertEquals(result.actions[1].actionTimetoken, 2000) + + assertEquals(result.actions[2].messageTimetoken, 789) + assertEquals(result.actions[2].type, "emoji") + assertEquals(result.actions[2].uuid, "uuid_2") + assertEquals(result.actions[2].value, "😄") + assertEquals(result.actions[2].actionTimetoken, 3000) + } + + + @Test + fun testAsyncSuccess() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.async { result, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNGetMessageActions, status.operation) + assertEquals(result!!.actions.size, 1) + assertEquals(result.actions[0].messageTimetoken, 123) + assertEquals(result.actions[0].type, "emoji") + assertEquals(result.actions[0].uuid, "someUuid") + assertEquals(result.actions[0].value, "smiley") + assertEquals(result.actions[0].actionTimetoken, 1000) + success.set(true) + } + + success.listen() + } + + @Test + fun testMalformedJson() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley" + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testEmptyBody() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn(emptyJson()) + ) + + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNoBody() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn(noContent()) + ) + + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testNoData() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200 + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testEmptyData() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200 + "data": [] + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + + @Test + fun testNoChannel() { + try { + pubnub.getMessageActions().apply { + + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testBlankChannel() { + try { + pubnub.getMessageActions().apply { + channel = " " + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testInvalidSubKey() { + pubnub.configuration.subscribeKey = " " + try { + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testAuthKeyRequired() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOptionalParams() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .withQueryParam("limit", matching("10")) + .withQueryParam("start", matching("15")) + .withQueryParam("end", matching("20")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.getMessageActions().apply { + channel = "coolChannel" + limit = 10 + start = 15 + end = 20 + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + assertEquals("10", requests[0].queryParameter("limit").firstValue()) + assertEquals("15", requests[0].queryParameter("start").firstValue()) + assertEquals("20", requests[0].queryParameter("end").firstValue()) + } + + @Test + fun testNoOptionalParams() { + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .withQueryParam("limit", absent()) + .withQueryParam("start", absent()) + .withQueryParam("end", absent()) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + assertFalse(requests[0].queryParameter("limit").isPresent) + assertFalse(requests[0].queryParameter("start").isPresent) + assertFalse(requests[0].queryParameter("end").isPresent) + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "123", + "type": "emoji", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.getMessageActions().apply { + channel = "coolChannel" + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNGetMessageActions, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_msga", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/ReceiveMessageActions.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/ReceiveMessageActions.kt new file mode 100644 index 000000000..1c59c6978 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/ReceiveMessageActions.kt @@ -0,0 +1,204 @@ +package com.pubnub.api.legacy.endpoints.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNub +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.listen +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class ReceiveMessageActions : BaseTest() { + + @Test + fun testReceiveMessageAction() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "1000", + "r": 12 + }, + "m": [ + { + "a": "1", + "f": 0, + "e": 3, + "i": "client-1639ed91", + "p": { + "t": "1000", + "r": 12 + }, + "k": "mySubscribeKey", + "c": "coolChannel", + "d": { + "source": "actions", + "version": "1.0", + "data": { + "messageTimetoken": "500", + "type": "reaction", + "value": "smiley", + "actionTimetoken": "600" + }, + "event": "added" + } + } + ] + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + failTest() + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + failTest() + } + + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) { + failTest() + } + + override fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) { + assertEquals(pnMessageActionResult.channel, "coolChannel") + assertEquals(pnMessageActionResult.messageAction.messageTimetoken, 500) + assertEquals(pnMessageActionResult.messageAction.uuid, "client-1639ed91") + assertEquals(pnMessageActionResult.messageAction.actionTimetoken, 600) + assertEquals(pnMessageActionResult.messageAction.type, "reaction") + assertEquals(pnMessageActionResult.messageAction.value, "smiley") + success.set(true) + } + }) + + pubnub.subscribe().apply { + channels = listOf("coolChannel") + }.execute() + + success.listen() + + } + + @Test + fun testReceiveMessageActionMulti() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "1000", + "r": 12 + }, + "m": [ + { + "a": "1", + "f": 0, + "e": 3, + "i": "client-1639ed91", + "p": { + "t": "1000", + "r": 12 + }, + "k": "mySubscribeKey", + "c": "coolChannel", + "d": { + "source": "actions", + "version": "1.0", + "data": { + "messageTimetoken": "500", + "type": "reaction", + "value": "smiley", + "actionTimetoken": "600" + }, + "event": "added" + } + }, + { + "a": "1", + "f": 0, + "e": 3, + "i": "client-1639ed91", + "p": { + "t": "1000", + "r": 12 + }, + "k": "mySubscribeKey", + "c": "coolChannel", + "d": { + "source": "actions", + "version": "1.0", + "data": { + "messageTimetoken": "500", + "type": "reaction", + "value": "grinning", + "actionTimetoken": "699" + }, + "event": "added" + } + } + ] + } + """.trimIndent() + ) + ) + ) + + val count = AtomicInteger() + val success = AtomicBoolean() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + failTest() + pnMessageResult.message + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + failTest() + } + + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) { + failTest() + } + + override fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) { + count.incrementAndGet() + if (count.get() == 2) { + success.set(true) + } + } + }) + + pubnub.subscribe().apply { + channels = listOf("coolChannel") + }.execute() + + success.listen() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/RemoveMessageActionEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/RemoveMessageActionEndpointTest.kt new file mode 100644 index 000000000..292cf1a2e --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/message_actions/RemoveMessageActionEndpointTest.kt @@ -0,0 +1,326 @@ +package com.pubnub.api.legacy.endpoints.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class RemoveMessageActionEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": {} + } + """.trimIndent() + ) + ) + ) + + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } + + @Test + fun testAsyncSuccess() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": {} + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNDeleteMessageAction, status.operation) + success.set(true) + } + + success.listen() + } + + @Test + fun testMalformedJson() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200 + "data": { + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + + @Test + fun testEmptyBody() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn(emptyJson()) + ) + + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + @Test + fun testNoBody() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn(noContent()) + ) + + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + @Test + fun testNoData() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200 + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + @Test + fun testNullData() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": null + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + } catch (e: Exception) { + failTest() + } + } + + @Test + fun testNoChannel() { + try { + pubnub.removeMessageAction().apply { + + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testBlankChannel() { + try { + pubnub.removeMessageAction().apply { + channel = " " + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testNoMessageTimetoken() { + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_TIMETOKEN_MISSING, e) + } + } + + @Test + fun testNoActionTimetoken() { + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 100L + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.MESSAGE_ACTION_TIMETOKEN_MISSING, e) + } + } + + @Test + fun testInvalidSubKey() { + pubnub.configuration.subscribeKey = " " + try { + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testAuthKeyRequired() { + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": {} + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "authKey" + + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.sync()!! + + val requests = findAll(deleteRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testTelemetryParam() { + val success = AtomicBoolean() + + stubFor( + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/coolChannel/message/123/action/100")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "data": {} + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + + lateinit var telemetryParamName: String + + pubnub.removeMessageAction().apply { + channel = "coolChannel" + messageTimetoken = 123L + actionTimetoken = 100L + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNDeleteMessageAction, status.operation) + telemetryParamName = "l_${status.operation.queryParam}" + assertEquals("l_msga", telemetryParamName) + success.set(true) + } + + success.listen() + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + success.set(true) + } + + success.listen() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/GetStateEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/GetStateEndpointTest.kt new file mode 100644 index 000000000..feb34f249 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/GetStateEndpointTest.kt @@ -0,0 +1,443 @@ +package com.pubnub.api.legacy.endpoints.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class GetStateEndpointTest : BaseTest() { + + @Test + fun testOneChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.sync()!! + + val ch1Data = result.stateByUUID["testChannel"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + } + + @Test + fun testOneChannelWithoutUUIDSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channels = listOf("testChannel") + }.sync()!! + + val ch1Data = result.stateByUUID["testChannel"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + } + + + @Test + fun testFailedPayloadSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { "status": 200, "message": "OK", "payload": "age" : 20 + """.trimIndent() + ) + ) + ) + + try { + pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.sync() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testMultipleChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1,ch2/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "ch1": { + "age": 20, + "status": "online" + }, + "ch2": { + "age": 100, + "status": "offline" + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channels = listOf("ch1", "ch2") + uuid = "sampleUUID" + }.sync()!! + + val ch1Data = result.stateByUUID["ch1"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + + val ch2Data = result.stateByUUID["ch2"]!! + assertEquals(pubnub.mapper.elementToInt(ch2Data, "age"), 100) + assertEquals(pubnub.mapper.elementToString(ch2Data, "status"), "offline") + } + + @Test + fun testOneChannelGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "chcg1": { + "age": 20, + "status": "online" + }, + "chcg2": { + "age": 100, + "status": "offline" + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channelGroups = listOf("cg1") + uuid = "sampleUUID" + }.sync()!! + + val ch1Data = result.stateByUUID["chcg1"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + + val ch2Data = result.stateByUUID["chcg2"]!! + assertEquals(pubnub.mapper.elementToInt(ch2Data, "age"), 100) + assertEquals(pubnub.mapper.elementToString(ch2Data, "status"), "offline") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun testManyChannelGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "chcg1": { + "age": 20, + "status": "online" + }, + "chcg2": { + "age": 100, + "status": "offline" + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channelGroups = listOf("cg1", "cg2") + uuid = "sampleUUID" + }.sync()!! + + val ch1Data = result.stateByUUID["chcg1"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + + val ch2Data = result.stateByUUID["chcg2"]!! + assertEquals(pubnub.mapper.elementToInt(ch2Data, "age"), 100) + assertEquals(pubnub.mapper.elementToString(ch2Data, "status"), "offline") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1,cg2", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun testCombinationSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "chcg1": { + "age": 20, + "status": "online" + }, + "chcg2": { + "age": 100, + "status": "offline" + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.getPresenceState().apply { + channels = listOf("ch1") + channelGroups = listOf("cg1", "cg2") + uuid = "sampleUUID" + }.sync()!! + + val ch1Data = result.stateByUUID["chcg1"]!! + assertEquals(pubnub.mapper.elementToInt(ch1Data, "age"), 20) + assertEquals(pubnub.mapper.elementToString(ch1Data, "status"), "online") + + val ch2Data = result.stateByUUID["chcg2"]!! + assertEquals(pubnub.mapper.elementToInt(ch2Data, "age"), 100) + assertEquals(pubnub.mapper.elementToString(ch2Data, "status"), "offline") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1,cg2", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun testMissingChannelAndGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.getPresenceState().apply { + uuid = "sampleUUID" + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_AND_GROUP_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.async { result, status -> + if (status.operation == PNOperationType.PNGetState) { + atomic.incrementAndGet() + } + } + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testBlankSubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = " " + + try { + pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptySubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/sampleUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = "" + + try { + pubnub.getPresenceState().apply { + channels = listOf("testChannel") + uuid = "sampleUUID" + }.sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/HereNowEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/HereNowEndpointTest.kt new file mode 100644 index 000000000..678a987b7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/HereNowEndpointTest.kt @@ -0,0 +1,513 @@ +package com.pubnub.api.legacy.endpoints.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError.SUBSCRIBE_KEY_MISSING +import com.pubnub.api.PubNubException +import com.pubnub.api.enums.PNOperationType.PNHereNowOperation +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.presence.PNHereNowResult +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.atomic.AtomicInteger + +class HereNowEndpointTest : BaseTest() { + + @Test + fun testMultipleChannelStateSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/ch1,ch2")).willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "total_occupancy": 3, + "total_channels": 2, + "channels": { + "ch1": { + "occupancy": 1, + "uuids": [ + { + "uuid": "user1", + "state": { + "age": 10 + } + } + ] + }, + "ch2": { + "occupancy": 2, + "uuids": [ + { + "uuid": "user1", + "state": { + "age": 10 + } + }, + { + "uuid": "user3", + "state": { + "age": 30 + } + } + ] + } + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("ch1", "ch2") + includeState = true + }.sync()!! + + assertEquals(response.totalChannels, 2) + assertEquals(response.totalOccupancy, 3) + + assertEquals(response.channels["ch1"]!!.channelName, "ch1") + assertEquals(response.channels["ch1"]!!.occupancy, 1) + assertEquals(response.channels["ch1"]!!.occupants.size, 1) + assertEquals(response.channels["ch1"]!!.occupants[0].uuid, "user1") + assertEquals(response.channels["ch1"]!!.occupants[0].state.toString(), """{"age":10}""") + + assertEquals(response.channels["ch2"]!!.channelName, "ch2") + assertEquals(response.channels["ch2"]!!.occupancy, 2) + assertEquals(response.channels["ch2"]!!.occupants.size, 2) + assertEquals(response.channels["ch2"]!!.occupants[0].uuid, "user1") + assertEquals(response.channels["ch2"]!!.occupants[0].state.toString(), """{"age":10}""") + assertEquals(response.channels["ch2"]!!.occupants[1].uuid, "user3") + assertEquals(response.channels["ch2"]!!.occupants[1].state.toString(), """{"age":30}""") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("state").firstValue()) + } + + @Test + fun testMultipleChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/ch1,ch2")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "total_occupancy": 3, + "total_channels": 2, + "channels": { + "ch1": { + "occupancy": 1, + "uuids": [ + { + "uuid": "user1" + } + ] + }, + "ch2": { + "occupancy": 2, + "uuids": [ + { + "uuid": "user1" + }, + { + "uuid": "user3" + } + ] + } + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("ch1", "ch2") + includeState = true + }.sync()!! + + assertEquals(response.totalChannels, 2) + assertEquals(response.totalOccupancy, 3) + + assertEquals(response.channels["ch1"]!!.channelName, "ch1") + assertEquals(response.channels["ch1"]!!.occupancy, 1) + assertEquals(response.channels["ch1"]!!.occupants.size, 1) + assertEquals(response.channels["ch1"]!!.occupants[0].uuid, "user1") + assertNull(response.channels["ch1"]!!.occupants[0].state) + + assertEquals(response.channels["ch2"]!!.channelName, "ch2") + assertEquals(response.channels["ch2"]!!.occupancy, 2) + assertEquals(response.channels["ch2"]!!.occupants.size, 2) + assertEquals(response.channels["ch2"]!!.occupants[0].uuid, "user1") + assertNull(response.channels["ch2"]!!.occupants[0].state) + assertEquals(response.channels["ch2"]!!.occupants[1].uuid, "user3") + assertNull(response.channels["ch2"]!!.occupants[1].state) + + val requests = findAll(getRequestedFor(anyUrl())) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("state").firstValue()) + } + + @Test + fun testMultipleChannelWithoutStateSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1,game2")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": { + "game1": { + "uuids": [ + "a3ffd012-a3b9-478c-8705-64089f24d71e" + ], + "occupancy": 1 + } + }, + "total_channels": 1, + "total_occupancy": 1 + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("game1", "game2") + includeState = false + }.sync()!! + + assertEquals(response.totalChannels, 1) + assertEquals(response.totalOccupancy, 1) + + assertEquals(response.channels["game1"]!!.channelName, "game1") + assertEquals(response.channels["game1"]!!.occupancy, 1) + assertEquals(response.channels["game1"]!!.occupants.size, 1) + assertEquals(response.channels["game1"]!!.occupants[0].uuid, "a3ffd012-a3b9-478c-8705-64089f24d71e") + assertNull(response.channels["game1"]!!.occupants[0].state) + } + + @Test + fun testMultipleChannelWithoutStateUUIDsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1,game2")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": { + "game1": { + "occupancy": 1 + } + }, + "total_channels": 1, + "total_occupancy": 1 + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("game1", "game2") + includeState = false + includeUUIDs = false + }.sync()!! + + assertEquals(response.totalChannels, 1) + assertEquals(response.totalOccupancy, 1) + + assertEquals(response.channels["game1"]!!.channelName, "game1") + assertEquals(response.channels["game1"]!!.occupancy, 1) + assertTrue(response.channels["game1"]!!.occupants.isEmpty()) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("disable_uuids").firstValue()) + } + + @Test + fun testSingularChannelWithoutStateUUIDsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "occupancy": 3 + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("game1") + includeUUIDs = false + includeUUIDs = false + }.sync()!! + + assertEquals(response.totalChannels, 1) + assertEquals(response.totalOccupancy, 3) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("disable_uuids").firstValue()) + } + + @Test + fun testSingularChannelWithoutStateSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "uuids": [ + "a3ffd012-a3b9-478c-8705-64089f24d71e" + ], + "occupancy": 1 + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("game1") + includeState = false + }.sync()!! + + assertEquals(response.totalChannels, 1) + assertEquals(response.totalOccupancy, 1) + assertEquals(response.channels.size, 1) + assertEquals(response.channels["game1"]!!.occupancy, 1) + assertEquals(response.channels["game1"]!!.occupants.size, 1) + assertEquals(response.channels["game1"]!!.occupants[0].uuid, "a3ffd012-a3b9-478c-8705-64089f24d71e") + assertEquals(response.channels["game1"]!!.occupants[0].state, null) + } + + @Test + fun testSingularChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "uuids": [ + { + "uuid": "a3ffd012-a3b9-478c-8705-64089f24d71e", + "state": { + "age": 10 + } + } + ], + "occupancy": 1 + } + """.trimIndent() + ) + ) + ) + val response = pubnub.hereNow().apply { + channels = listOf("game1") + includeState = true + }.sync()!! + + assertEquals(response.totalChannels, 1) + assertEquals(response.totalOccupancy, 1) + assertEquals(response.channels.size, 1) + assertEquals(response.channels["game1"]!!.occupancy, 1) + assertEquals(response.channels["game1"]!!.occupants.size, 1) + assertEquals(response.channels["game1"]!!.occupants[0].uuid, "a3ffd012-a3b9-478c-8705-64089f24d71e") + assertEquals(response.channels["game1"]!!.occupants[0].state.toString(), """{"age":10}""") + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("state").firstValue()) + } + + @Test + fun testSingularChannelAndGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/game1")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": {}, + "total_channels": 0, + "total_occupancy": 0 + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.hereNow().apply { + channels = listOf("game1") + channelGroups = listOf("grp1") + includeState = true + }.sync()!! + + assertEquals(response.totalOccupancy, 0) + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/ch1,ch2")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "total_occupancy": 3, + "total_channels": 2, + "channels": { + "ch1": { + "occupancy": 1, + "uuids": [ + { + "uuid": "user1", + "state": { + "age": 10 + } + } + ] + }, + "ch2": { + "occupancy": 2, + "uuids": [ + { + "uuid": "user1", + "state": { + "age": 10 + } + }, + { + "uuid": "user3", + "state": { + "age": 30 + } + } + ] + } + } + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.hereNow().apply { + channels = listOf("ch1", "ch2") + includeState = true + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathMatching("/v2/presence/sub_key/mySubscribeKey.*")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": { + "ch1": { + "occupancy": 1, + "uuids": [ + "myUUID" + ] + } + }, + "total_channels": 1, + "total_occupancy": 1 + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.hereNow().async { result, status -> + if (status.operation == PNHereNowOperation) { + assertEquals(1, result!!.channels.size) + assertEquals(pubnub.configuration.uuid, result.channels["ch1"]!!.occupants[0].uuid) + atomic.incrementAndGet(); + } + } + + Awaitility.await().atMost(5, SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testEmptySubKeySync() { + pubnub.configuration.subscribeKey = "" + + var response: PNHereNowResult? = null + try { + response = pubnub.hereNow().apply { + channels = listOf("ch1", "ch2") + includeState = true + }.sync() + } catch (e: Exception) { + assertNull(response) + assertTrue((e as PubNubException).pubnubError == SUBSCRIBE_KEY_MISSING) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/LeaveTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/LeaveTest.kt new file mode 100644 index 000000000..ca05735cf --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/LeaveTest.kt @@ -0,0 +1,295 @@ +package com.pubnub.api.legacy.endpoints.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.endpoints.presence.Leave +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +class LeaveTest : BaseTest() { + + @Test + fun subscribeChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channels = listOf("coolChannel") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun subscribeChannelsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel,coolChannel2/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + + @Test + fun subscribeChannelsWithGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel,coolChannel2/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + channelGroups = listOf("cg1") + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun subscribeChannelsWithGroupASync() { + val statusArrived = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel,coolChannel2/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + channelGroups = listOf("cg1") + }.async { _, status -> + assertEquals(status.affectedChannels[0], "coolChannel") + assertEquals(status.affectedChannels[1], "coolChannel2") + assertEquals(status.affectedChannelGroups[0], "cg1") + statusArrived.set(true) + } + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAtomic( + statusArrived, + IsEqual.equalTo(true) + ) + } + + @Test + fun subscribeGroupsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channelGroups = listOf("cg1", "cg2") + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1,cg2", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun subscribeGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + Leave(pubnub).apply { + channelGroups = listOf("cg1") + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun testMissingChannelAndGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + try { + Leave(pubnub).apply {}.sync() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_AND_GROUP_MISSING, e) + } + } + + @Test + fun testBlankSubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = " " + + try { + Leave(pubnub).apply {}.sync() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptySubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = "" + + try { + Leave(pubnub).apply {}.sync() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/coolChannel/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + Leave(pubnub).apply { + channels = listOf("coolChannel") + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/SetStateEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/SetStateEndpointTest.kt new file mode 100644 index 000000000..95a54032b --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/SetStateEndpointTest.kt @@ -0,0 +1,449 @@ +package com.pubnub.api.legacy.endpoints.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SetStateEndpointTest : BaseTest() { + + @Test + fun applyStateForChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state!!, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun applyStateForSomebodyElseChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/someoneElseUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + uuid = "someoneElseUUID" + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state!!, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun applyStateForChannelsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel,testChannel2/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channels = listOf("testChannel", "testChannel2") + state = mapOf("age" to 20) + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + + @Test + fun applyStateForChannelGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channelGroups = listOf("cg1") + state = mapOf("age" to 20) + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state!!, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun applyStateForChannelGroupsSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/,/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channelGroups = listOf("cg1", "cg2") + state = mapOf("age" to 20) + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state!!, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1,cg2", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun applyStateForMixSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val result = pubnub.setPresenceState().apply { + channels = listOf("ch1") + channelGroups = listOf("cg1", "cg2") + state = mapOf("age" to 20) + }.sync()!! + + assertEquals(pubnub.mapper.elementToInt(result.state!!, "age"), 20) + assertEquals(pubnub.mapper.elementToString(result.state, "status"), "online") + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun applyNon200Sync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ).withStatus(400) + ) + ) + + try { + pubnub.setPresenceState().apply { + channels = listOf("ch1") + channelGroups = listOf("cg1", "cg2") + state = mapOf("age" to 20) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.HTTP_ERROR, e) + } + } + + @Test + fun missingStateSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.setPresenceState().apply { + channels = listOf("testChannel") + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.STATE_MISSING, e) + } + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testBlankSubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("pnsdk", matching("PubNub-Java-Unified/suchJava")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + "{ \"status\": 200, \"message\": \"OK\", \"payload\": { \"age\" : " + + "20, \"status\" : \"online\" }, \"service\": \"Presence\"}" + ) + ) + ) + + pubnub.configuration.subscribeKey = " " + + try { + pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptySubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("pnsdk", matching("PubNub-Java-Unified/suchJava")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + "{ \"status\": 200, \"message\": \"OK\", \"payload\": { \"age\" : " + + "20, \"status\" : \"online\" }, \"service\": \"Presence\"}" + ) + ) + ) + + pubnub.configuration.subscribeKey = "" + + try { + pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testChannelAndGroupMissingSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "age": 20, + "status": "online" + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.setPresenceState().apply { + state = mapOf("age" to 20) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.CHANNEL_AND_GROUP_MISSING, e) + } + } + + @Test + fun testNullPayloadSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/testChannel/uuid/myUUID/data")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("state", equalToJson("""{"age":20}""")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.setPresenceState().apply { + channels = listOf("testChannel") + state = mapOf("age" to 20) + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/WhereNowEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/WhereNowEndpointTest.kt new file mode 100644 index 000000000..82dae9571 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/presence/WhereNowEndpointTest.kt @@ -0,0 +1,376 @@ +package com.pubnub.api.legacy.endpoints.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.atomic.AtomicInteger + +class WhereNowEndpointTest : BaseTest() { + + @Test + fun testSyncSuccess() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.whereNow().sync()!! + assertThat(response.channels, Matchers.contains("a", "b")) + } + + @Test + fun testSyncSuccessCustomUUID() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/customUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val response = pubnub.whereNow().apply { + uuid = "customUUID" + }.sync()!! + + assertThat(response.channels, Matchers.contains("a", "b")) + } + + @Test + fun testSyncBrokenWithString() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\"" + + ": [zimp]}, \"service\": \"Presence\"}" + ) + ) + ) + + try { + pubnub.whereNow().sync() + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testSyncBrokenWithoutJSON() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\": " + + "zimp}, \"service\": \"Presence\"}" + ) + ) + ) + + try { + pubnub.whereNow().sync() + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testSyncBrokenWithout200() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse() + .withStatus(404) + .withBody( + "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\": [\"a\",\"b\"]}," + + " \"service\": \"Presence\"}" + ) + ) + ) + + try { + pubnub.whereNow().sync() + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.HTTP_ERROR, e) + } + } + + @Test + fun testAsyncSuccess() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.whereNow().async { result, status -> + assertFalse(status.error) + assertThat(result!!.channels, Matchers.contains("a", "b")) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testAsyncBrokenWithString() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\": " + + "[zimp]}, \"service\": \"Presence\"}" + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.whereNow().async { result, status -> + assertTrue(status.error) + assertPnException(PubNubError.PARSING_ERROR, status) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testAsyncBrokenWithoutJSON() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + "{\"status\": 200, \"message\": \"OK\", \"payload\": {\"channels\": " + + "zimp}, \"service\": \"Presence\"}" + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.whereNow().async { _, status -> + assertTrue(status.error) + assertPnException(PubNubError.PARSING_ERROR, status) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testAsyncBrokenWithout200() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse() + .withStatus(400) + .withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + val atomic = AtomicInteger(0) + + pubnub.whereNow().async { _, status -> + assertTrue(status.error) + assertPnException(PubNubError.HTTP_ERROR, status) + assertEquals(PNStatusCategory.PNBadRequestCategory, status.category) + atomic.incrementAndGet() + } + + Awaitility.await().atMost(5, SECONDS).untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testIsAuthRequiredSuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.whereNow().sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testBlankSubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = " " + + try { + pubnub.whereNow().sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptySubKeySync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "a", + "b" + ] + }, + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.subscribeKey = "" + + try { + pubnub.whereNow().sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testNullPayloadSync() { + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + try { + pubnub.whereNow().sync()!! + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/PublishTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/PublishTest.kt new file mode 100644 index 000000000..00926ba3a --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/PublishTest.kt @@ -0,0 +1,550 @@ +package com.pubnub.api.legacy.endpoints.pubsub + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.assertPnException +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.failTest +import com.pubnub.api.legacy.BaseTest +import org.awaitility.Awaitility +import org.hamcrest.core.IsEqual +import org.json.JSONArray +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.nio.charset.Charset +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class PublishTest : BaseTest() { + + @Test + fun testFireSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.fire().apply { + channel = "coolChannel" + message = "hi" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals("true", requests[0].queryParameter("norep").firstValue()) + assertEquals("0", requests[0].queryParameter("store").firstValue()) + } + + @Test + fun testNoRepSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + replicate = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals("true", requests[0].queryParameter("norep").firstValue()) + } + + @Test + fun testRepDefaultSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hirep%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hirep" + replicate = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals("true", requests[0].queryParameter("norep").firstValue()) + } + + @Test + fun testSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + replicate = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessSequenceSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + }.sync()!! + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(2, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals("1", requests[0].queryParameter("seqn").firstValue()) + assertEquals("2", requests[1].queryParameter("seqn").firstValue()) + } + + @Test + fun testSuccessPostSync() { + stubFor( + post(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = listOf("m1", "m2") + usePost = true + }.sync()!! + + val requests = findAll(postRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals("""["m1","m2"]""", String(requests[0].body, Charset.forName("UTF-8"))) + } + + @Test + fun testSuccessStoreFalseSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + shouldStore = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("0", requests[0].queryParameter("store").firstValue()) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessStoreTrueSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + shouldStore = true + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", requests[0].queryParameter("store").firstValue()) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessMetaSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam( + "pnsdk", matching("PubNub-Kotlin/.*") + ) //.withQueryParam("meta", matching("%5B%22m1%22%2C%22m2%22%5D")) + .withQueryParam("meta", equalToJson("""["m1","m2"]""")) + .withQueryParam("store", matching("0")) + .withQueryParam("seqn", matching("1")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + meta = listOf("m1", "m2") + shouldStore = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun testSuccessAuthKeySync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + pubnub.configuration.authKey = "authKey" + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessIntSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/10")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = 10 + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessArraySync() { + stubFor( + get( + urlPathEqualTo( + "/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/[%22a%22,%22b%22,%22c%22]" + ) + ).willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + val l = listOf("a", "b", "c") + + pubnub.publish().apply { + channel = "coolChannel" + message = l + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessArrayEncryptedSync() { + stubFor( + get( + urlPathEqualTo( + "/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22HFP7V6bDwBLrwc1t8Rnrog==%22" + ) + ) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + pubnub.configuration.cipherKey = "testCipher" + + pubnub.publish().apply { + channel = "coolChannel" + message = listOf("m1", "m2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testSuccessPostEncryptedSync() { + stubFor( + post(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + pubnub.configuration.cipherKey = "testCipher" + + pubnub.publish().apply { + channel = "coolChannel" + message = listOf("m1", "m2") + usePost = true + }.sync()!! + + val requests = findAll(postRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + assertEquals(""""HFP7V6bDwBLrwc1t8Rnrog=="""", String(requests[0].body, Charset.forName("UTF-8"))) + } + + @Test + fun testSuccessHashMapSync() { + val params: MutableMap = HashMap() + params["a"] = 10 + params["z"] = "test" + stubFor( + get( + urlPathEqualTo( + "/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%7B%22a%22:10,%22z%22:%22test%22%7D" + ) + ) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = params + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + private class TestPojo() { + + constructor(s1: String, s2: String) : this() { + field1 = s1 + field2 = s2 + } + + private var field1: String? = null + private var field2: String? = null + + } + + @Test + fun testSuccessPOJOSync() { + val testPojo = TestPojo("10", "20") + stubFor( + get( + urlPathEqualTo( + "/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%7B%22field1%22:%2210%22,%22field2%22:%2220%22%7D" + ) + ) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = testPojo + }.sync()!! + + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testJSONObject() { + val testMessage = JSONObject() + testMessage.put("hi", "test") + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%7B%22hi%22:%22test%22%7D")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = testMessage.toMap() + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testJSONList() { + val testMessage = JSONArray() + testMessage.put("hi") + testMessage.put("hi2") + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/[%22hi%22,%22hi2%22]")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = testMessage.toList() + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myUUID", requests[0].queryParameter("uuid").firstValue()) + } + + @Test + fun testMissingChannel() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + try { + pubnub.publish().apply { + message = "hi" + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testEmptyChannel() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","158832720000000000"]""")) + ) + + try { + pubnub.publish().apply { + channel = " " + message = "hi" + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testMissingMessage() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + try { + pubnub.publish().apply { + channel = "coolChannel" + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.MESSAGE_MISSING, e) + } + } + + @Test + fun testOperationTypeSuccessAsync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + val atomic = AtomicInteger(0) + + pubnub.publish().apply { + + }.async { _, status -> + if (status.operation === PNOperationType.PNPublishOperation) { + atomic.incrementAndGet() + } + } + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testEmptySubKeySync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hirep%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.configuration.subscribeKey = "" + + try { + pubnub.publish().apply { + channel = "coolChannel" + message = "hirep" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testEmptyPublishKeySync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hirep%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + pubnub.configuration.publishKey = "" + + try { + pubnub.publish().apply { + channel = "coolChannel" + message = "hirep" + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PUBLISH_KEY_MISSING, e) + } + } + + @Test + fun testTTLShouldStoryDefaultSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + ttl = 10 + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("10", requests[0].queryParameter("ttl").firstValue()) + } + + @Test + fun testTTLShouldStoreFalseSuccessSync() { + stubFor( + get(urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/coolChannel/0/%22hi%22")) + .willReturn(aResponse().withBody("""[1,"Sent","15883272000000000"]""")) + ) + + pubnub.publish().apply { + channel = "coolChannel" + message = "hi" + shouldStore = false + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("0", requests[0].queryParameter("store").firstValue()) + assertFalse(requests[0].queryParameter("ttl").isPresent) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SignalTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SignalTest.kt new file mode 100644 index 000000000..b2ad1a906 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SignalTest.kt @@ -0,0 +1,223 @@ +package com.pubnub.api.legacy.endpoints.pubsub + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import com.pubnub.api.models.consumer.pubsub.PNSignalResult +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult +import okhttp3.HttpUrl +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +class SignalTest : BaseTest() { + + @Test + fun testSignalGetSuccessSync() { + stubFor( + get(urlMatching("/signal/myPublishKey/mySubscribeKey/0/coolChannel.*")) + .willReturn( + aResponse() + .withBody( + """ + [ + 1, + "Sent", + "1000" + ] + """.trimIndent() + ) + ) + ) + + + val payload = mapOf("text" to "hello") + + pubnub.signal().apply { + channel = "coolChannel" + message = payload + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/signal.*"))) + assertEquals(1, requests.size) + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + + val httpUrl = HttpUrl.parse(request.absoluteUrl) + var decodedSignalPayload: String? = null + if (httpUrl != null) { + decodedSignalPayload = httpUrl.pathSegments()[httpUrl.pathSize() - 1] + } + assertEquals(pubnub.mapper.toJson(payload), decodedSignalPayload) + } + + @Test + fun testSignalGetSuccessAsync() { + stubFor( + get(urlMatching("/signal/myPublishKey/mySubscribeKey/0/coolChannel.*")) + .willReturn( + aResponse() + .withBody( + """ + [ + 1, + "Sent", + "1000" + ] + """.trimIndent() + ) + ) + ) + + val payload = UUID.randomUUID().toString() + + val success = AtomicBoolean() + + pubnub.signal().apply { + channel = "coolChannel" + message = payload + }.async { result, status -> + result!! + assertFalse(status.error) + assertEquals(PNOperationType.PNSignalOperation, status.operation) + assertEquals("1000", result.timetoken.toString()) + success.set(true) + } + + success.listen() + } + + @Test + fun testSignalSuccessReceive() { + stubFor( + get(urlMatching("/v2/subscribe/mySubscribeKey/coolChannel/0.*")) + .willReturn( + aResponse().withBody( + """ + { + "m": [ + { + "c": "coolChannel", + "f": "0", + "i": "uuid", + "d": "hello", + "e": 1, + "p": { + "t": 1000, + "r": 1 + }, + "k": "mySubscribeKey", + "b": "coolChannel" + } + ], + "t": { + "r": "56", + "t": 1000 + } + } + """.trimIndent() + ) + ) + ) + + val success = AtomicBoolean() + + + + pubnub.addListener(object : SubscribeCallback() { + override fun signal(pubnub: PubNub, pnSignalResult: PNSignalResult) { + assertEquals("coolChannel", pnSignalResult.channel) + assertEquals("hello", pnSignalResult.message.asString) + assertEquals("uuid", pnSignalResult.publisher) + success.set(true) + } + + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) {} + override fun messageAction(pubnub: PubNub, pnMessageActionResult: PNMessageActionResult) {} + }) + + pubnub.subscribe().apply { + channels = listOf("coolChannel") + }.execute() + + success.listen() + } + + @Test + fun testSignalFailNoChannel() { + try { + pubnub.signal().apply { + message = UUID.randomUUID().toString() + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testSignalFailBlankChannel() { + try { + pubnub.signal().apply { + channel = " " + message = UUID.randomUUID().toString() + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testSignalFailNoMessage() { + try { + pubnub.signal().apply { + channel = UUID.randomUUID().toString() + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.MESSAGE_MISSING, e) + } + } + + @Test + fun testSignalTelemetryParam() { + stubFor( + get(urlMatching("/signal/myPublishKey/mySubscribeKey/0/coolChannel.*")).willReturn( + aResponse().withBody( + """ + [ + 1, + "Sent", + "1000" + ] + """.trimIndent() + ) + ) + ) + stubFor( + get(urlMatching("/time/0.*")) + .willReturn(aResponse().withBody("[1000]")) + ) + pubnub.signal().apply { + channel = "coolChannel" + message = UUID.randomUUID().toString() + }.sync()!! + + pubnub.time() + .sync() + + val requests = findAll(getRequestedFor(urlMatching("/time/0.*"))) + assertEquals(1, requests.size) + val request = requests[0] + assertTrue(request.queryParameter("l_sig").isPresent) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SubscribeEndpointTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SubscribeEndpointTest.kt new file mode 100644 index 000000000..fb0433376 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/pubsub/SubscribeEndpointTest.kt @@ -0,0 +1,557 @@ +package com.pubnub.api.legacy.endpoints.pubsub + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.PubNubException +import com.pubnub.api.assertPnException +import com.pubnub.api.endpoints.pubsub.Subscribe +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.server.SubscribeMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SubscribeEndpointTest : BaseTest() { + + @Test + fun subscribeChannelSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "someSubKey", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + val subscribeEnvelope = Subscribe(pubnub).apply { + channels = listOf("coolChannel") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", subscribeEnvelope.metadata.region) + assertEquals(14607577960932487L, subscribeEnvelope.metadata.timetoken) + assertEquals(1, subscribeEnvelope.messages.size) + + val subscribeMessage: SubscribeMessage = subscribeEnvelope.messages[0] + assertEquals("4", subscribeMessage.shard) + assertEquals("0", subscribeMessage.flags) + assertEquals("coolChannel", subscribeMessage.channel) + assertEquals("coolChan-bnel", subscribeMessage.subscriptionMatch) + assertEquals("someSubKey", subscribeMessage.subscribeKey) + assertEquals("Client-g5d4g", subscribeMessage.issuingClientId) + assertEquals("""{"text":"Enter Message Here"}""", subscribeMessage.payload.toString()) + } + + @Test + fun subscribeChannelsSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "someSubKey", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + val subscribeEnvelope = Subscribe(pubnub).apply { + channels = listOf("coolChannel") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("1", subscribeEnvelope.metadata.region) + assertTrue(subscribeEnvelope.metadata.timetoken == 14607577960932487L) + assertEquals(1, subscribeEnvelope.messages.size) + + val subscribeMessage: SubscribeMessage = subscribeEnvelope.messages[0] + assertEquals("4", subscribeMessage.shard) + assertEquals("0", subscribeMessage.flags) + assertEquals("coolChannel", subscribeMessage.channel) + assertEquals("coolChan-bnel", subscribeMessage.subscriptionMatch) + assertEquals("someSubKey", subscribeMessage.subscribeKey) + assertEquals("Client-g5d4g", subscribeMessage.issuingClientId) + assertEquals("""{"text":"Enter Message Here"}""", subscribeMessage.payload.toString()) + } + + @Test + fun subscribeChannelsAuthSync() { + pubnub.configuration.authKey = "authKey" + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel,coolChannel2/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("authKey", requests[0].queryParameter("auth").firstValue()) + + } + + @Test + fun subscribeChannelsWithGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel,coolChannel2/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + channelGroups = listOf("cg1") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + fun subscribeGroupsSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channelGroups = listOf("cg1", "cg2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1,cg2", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + @Throws(PubNubException::class) + fun subscribeGroupSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channelGroups = listOf("cg1") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + } + + @Test + @Throws(PubNubException::class) + fun subscribeWithTimeTokenSync() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channelGroups = listOf("cg1") + timetoken = 1337L + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + assertEquals("1337", requests[0].queryParameter("tt").firstValue()) + } + + @Test + fun subscribeWithFilter() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("pnsdk", matching("PubNub-Kotlin.*")) + .withQueryParam("filter-expr", matching("this=1&that=cool")) + .withQueryParam("channel-group", matching("cg1")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channelGroups = listOf("cg1") + filterExpression = "this=1&that=cool" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun subscribeWithRegion() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channelGroups = listOf("cg1") + region = "10" + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("cg1", requests[0].queryParameter("channel-group").firstValue()) + assertEquals("10", requests[0].queryParameter("tr").firstValue()) + } + + @Test + fun subscribeMissingChannelAndGroupSync() { + try { + Subscribe(pubnub).apply { + + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_AND_GROUP_MISSING, e) + } + } + + @Test + fun testMissingSubKeySync() { + pubnub.configuration.subscribeKey = " " + + try { + Subscribe(pubnub).apply { + + }.sync()!! + throw RuntimeException() + } catch (e: PubNubException) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun stopAndReconnect() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/coolChannel,coolChannel2/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + Subscribe(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + }.sync()!! + + pubnub.disconnect() + pubnub.reconnect() + + Subscribe(pubnub).apply { + channels = listOf("coolChannel", "coolChannel2") + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(2, requests.size) + } + + @Test + fun testSuccessIncludeState() { + pubnub.configuration.presenceTimeout = 123 + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch1,ch2/0")) + .willReturn(aResponse().withStatus(200)) + ) + + try { + Subscribe(pubnub).apply { + channels = listOf("ch1", "ch2") + state = mapOf( + "CH1" to "this-is-channel1", + "CH2" to "this-is-channel2" + ) + }.sync() + } catch (e: Exception) { + // e.printStackTrace() + } + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + + val request = requests[0] + assertEquals("myUUID", request.queryParameter("uuid").firstValue()) + assertEquals("123", request.queryParameter("heartbeat").firstValue()) + assertEquals( + """{"CH1":"this-is-channel1","CH2":"this-is-channel2"}""", + request.queryParameter("state").firstValue() + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt new file mode 100644 index 000000000..addde449c --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/AddChannelsToPushTest.kt @@ -0,0 +1,228 @@ +package com.pubnub.api.legacy.endpoints.push + +import com.github.tomakehurst.wiremock.client.WireMock +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class AddChannelsToPushTest : BaseTest() { + + @Test + fun testAddAppleSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) + Assertions.assertEquals("apns", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testAddFirebaseSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) + Assertions.assertEquals("gcm", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testAddMicrosoftSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.MPNS + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) + Assertions.assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testAddApns2SuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + channels = listOf("ch1", "ch2", "ch3") + topic = "topic" + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("ch1,ch2,ch3", requests[0].queryParameter("add").firstValue()) + Assertions.assertEquals("development", requests[0].queryParameter("environment").firstValue()) + Assertions.assertEquals("topic", requests[0].queryParameter("topic").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("type").isPresent) + } + + @Test + fun testIsAuthRequiredSuccessAdd() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessAdd() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + val success = AtomicBoolean() + + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + }.async { _, status -> + Assertions.assertFalse(status.error) + Assertions.assertEquals(PNOperationType.PNAddPushNotificationsOnChannelsOperation, status.operation) + Assertions.assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertEquals(status.affectedChannels, listOf("ch1", "ch2", "ch3")) + Assertions.assertTrue(status.affectedChannelGroups.isEmpty()) + success.set(true) + } + + success.listen() + } + + + @Test + fun testEmptySubscribeKeyAdd() { + pubnub.configuration.subscribeKey = "" + + try { + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testNullPushTypeAdd() { + try { + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.PUSH_TYPE_MISSING, e) + } + } + + @Test + fun testNullDeviceIdAdd() { + try { + pubnub.addPushNotificationsOnChannels().apply { + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testEmptyDeviceIdAdd() { + try { + pubnub.addPushNotificationsOnChannels().apply { + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2", "ch3") + deviceId = " " + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testMissingChannelsAdd() { + try { + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testEmptyChannelsAdd() { + try { + pubnub.addPushNotificationsOnChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = emptyList() + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt new file mode 100644 index 000000000..1ea708f96 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/ListPushProvisionsTest.kt @@ -0,0 +1,320 @@ +package com.pubnub.api.legacy.endpoints.push + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +class ListPushProvisionsTest : BaseTest() { + + @Test + fun testAppleSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + topic = "irrelevant" + }.sync()!! + + assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("apns", requests[0].queryParameter("type").firstValue()) + assertFalse(requests[0].queryParameter("environment").isPresent) + assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testFirebaseSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + topic = "irrelevant" + }.sync()!! + + assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("gcm", requests[0].queryParameter("type").firstValue()) + assertFalse(requests[0].queryParameter("environment").isPresent) + assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testMicrosoftSuccessSync() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.MPNS + topic = "irrelevant" + }.sync()!! + + assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + assertFalse(requests[0].queryParameter("environment").isPresent) + assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testApns2SuccessSync() { + stubFor( + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + topic = "news" + }.sync()!! + + assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("development", requests[0].queryParameter("environment").firstValue()) + assertEquals("news", requests[0].queryParameter("topic").firstValue()) + } + + @Test + fun testApns2SuccessSync_Environment() { + stubFor( + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + topic = "news" + environment = PNPushEnvironment.PRODUCTION + }.sync()!! + + assertEquals(listOf("ch1", "ch2", "ch3"), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("production", requests[0].queryParameter("environment").firstValue()) + assertEquals("news", requests[0].queryParameter("topic").firstValue()) + } + + @Test + fun testEmptyArray() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("[]")) + ) + + val response = pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync()!! + + assertEquals(emptyList(), response.channels) + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + } + + @Test + fun testEmptyBody() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("")) + ) + + try { + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync()!! + failTest() + } catch (e: Exception) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testMalformedResponseSync() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(ok().withBody("{")) + ) + + try { + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.PARSING_ERROR, e) + } + } + + @Test + fun testMalformedResponseAsync() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(ok().withBody("{")) + ) + + val success = AtomicBoolean() + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.async { _, status -> + assertPnException(PubNubError.PARSING_ERROR, status) + assertEquals(PNStatusCategory.PNMalformedResponseCategory, status.category) + success.set(true) + } + success.listen() + } + + + @Test + fun testIsAuthRequiredSuccess() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccess() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + val success = AtomicBoolean() + + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + }.async { _, status -> + assertFalse(status.error) + assertEquals(PNOperationType.PNPushNotificationEnabledChannelsOperation, status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertTrue(status.affectedChannels.isEmpty()) + Assertions.assertTrue(status.affectedChannelGroups.isEmpty()) + success.set(true) + } + + success.listen() + } + + @Test + fun testNullSubscribeKey() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + pubnub.configuration.subscribeKey = " " + + try { + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + }.sync()!! + failTest("Didn't throw SUBSCRIBE_KEY_MISSING") + } catch (e: Exception) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testValidationNoDeviceId() { + try { + pubnub.auditPushChannelProvisions().apply { + pushType = PNPushType.FCM + }.sync()!! + failTest("Should throw no device id") + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testValidationNoPushTYpe() { + try { + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + }.sync()!! + failTest("Should throw no push type") + } catch (e: PubNubException) { + assertPnException(PubNubError.PUSH_TYPE_MISSING, e) + } + } + + @Test + fun testValidationNoTopic() { + try { + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + }.sync()!! + failTest("Should throw no topic") + } catch (e: PubNubException) { + assertPnException(PubNubError.PUSH_TOPIC_MISSING, e) + } + } + + @Test + fun testValidationDefaultEnvironment() { + stubFor( + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice")) + .willReturn(aResponse().withBody("""["ch1", "ch2", "ch3"]""")) + ) + + pubnub.auditPushChannelProvisions().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + topic = UUID.randomUUID().toString() + }.sync()!! + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + assertEquals(1, requests.size) + assertEquals("development", requests[0].queryParameter("environment").firstValue()) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt new file mode 100644 index 000000000..2732bb117 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/PushPayloadHelperHelperTest.kt @@ -0,0 +1,490 @@ +package com.pubnub.api.legacy.endpoints.push + +import com.pubnub.api.enums.PNPushEnvironment +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper +import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.* +import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.APNSPayload.APNS2Configuration +import com.pubnub.api.models.consumer.push.payload.PushPayloadHelper.APNSPayload.APS +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.* + +class PushPayloadHelperHelperTest : BaseTest() { + + @Test + fun testPayloads_Missing() { + val pushPayloadHelper = PushPayloadHelper() + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testPayloads_Null() { + val pushPayloadHelper = PushPayloadHelper() + pushPayloadHelper.apnsPayload = null + pushPayloadHelper.commonPayload = null + pushPayloadHelper.fcmPayload = null + pushPayloadHelper.mpnsPayload = null + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testPayloads_Empty() { + val pushPayloadHelper = PushPayloadHelper() + pushPayloadHelper.apnsPayload = APNSPayload() + pushPayloadHelper.commonPayload = HashMap() + pushPayloadHelper.fcmPayload = FCMPayload() + pushPayloadHelper.mpnsPayload = MPNSPayload() + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testApple_Empty() { + val pushPayloadHelper = PushPayloadHelper() + val apnsPayload = APNSPayload() + apnsPayload.aps = APS() + apnsPayload.apns2Configurations = ArrayList() + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testApple_Valid() { + val pushPayloadHelper = PushPayloadHelper() + + val apnsPayload = APNSPayload() + + val aps = APS() + aps.alert = "alert" + aps.badge = 5 + + apnsPayload.aps = aps + apnsPayload.apns2Configurations = emptyList() + apnsPayload.custom = hashMapOf( + "key_1" to "1", + "key_2" to 2 + ) + pushPayloadHelper.apnsPayload = apnsPayload + + val map = pushPayloadHelper.build() + + val pnApnsDataMap = map["pn_apns"] as Map<*, *> + assertEquals("1", pnApnsDataMap["key_1"]) + assertEquals(2, pnApnsDataMap["key_2"]) + + val apsMap = pnApnsDataMap["aps"] as Map<*, *> + assertEquals("alert", apsMap["alert"]) + assertEquals(5, apsMap["badge"]) + + val pushList = pnApnsDataMap["pn_push"] as List<*> + assertEquals(0, pushList.size) + } + + @Test + fun testApple_Aps() { + val pushPayloadHelper = PushPayloadHelper() + val apnsPayload = APNSPayload() + + apnsPayload.aps = APS().apply { + alert = "alert" + badge = 5 + } + pushPayloadHelper.apnsPayload = apnsPayload + + apnsPayload.custom = mapOf("key_2" to "2") + + val map = pushPayloadHelper.build() + + val pnApnsDataMap = map["pn_apns"] as Map<*, *> + val apsMap = pnApnsDataMap["aps"] as Map<*, *> + + assertEquals("alert", apsMap["alert"]) + assertEquals(5, apsMap["badge"]) + assertFalse(apsMap.containsKey("sound")) + assertFalse(pnApnsDataMap.containsKey("pn_push")) + assertFalse(pnApnsDataMap.containsKey("key_1")) + assertEquals("2", pnApnsDataMap["key_2"]) + } + + @Test + fun testApple_PnPushArray() { + val pushPayloadHelper = PushPayloadHelper() + + val target1 = APNS2Configuration.Target().apply { + environment = PNPushEnvironment.DEVELOPMENT + topic = "topic_1" + } + val target2 = APNS2Configuration.Target().apply { + environment = PNPushEnvironment.PRODUCTION + } + val target3 = APNS2Configuration.Target().apply { + environment = PNPushEnvironment.PRODUCTION + topic = "topic_3" + excludeDevices = listOf("ex_1", "ex_2") + } + val target4 = APNS2Configuration.Target().apply { + environment = null + topic = null + excludeDevices = null + } + val target5 = APNS2Configuration.Target().apply { + environment = PNPushEnvironment.PRODUCTION + topic = "topic_5" + excludeDevices = listOf() + } + val target6 = APNS2Configuration.Target().apply { + topic = "topic_6" + excludeDevices = null + } + val target7 = APNS2Configuration.Target() + + val apns2Config1 = APNS2Configuration().apply { + collapseId = "collapse_1" + expiration = "exp_1" + version = "v1" + targets = null + } + val apns2Config2 = APNS2Configuration().apply { + collapseId = "collapse_2" + expiration = "exp_2" + version = "v2" + targets = emptyList() + } + val apns2Config3 = APNS2Configuration().apply { + collapseId = null + expiration = "" + version = "v3" + targets = listOf(target1, target2, target3, target4, target5, target6, target7) + } + val apns2Config4 = APNS2Configuration().apply { + collapseId = null + expiration = null + version = null + } + val apns2Config5 = APNS2Configuration() + + pushPayloadHelper.apnsPayload = APNSPayload().apply { + apns2Configurations = listOf( + apns2Config1, apns2Config2, apns2Config3, apns2Config4, apns2Config5 + ) + } + + val map = pushPayloadHelper.build() + + val apnsMap = map["pn_apns"] as Map<*, *> + val pnPushList = apnsMap["pn_push"] as List> + + assertEquals(3, pnPushList.size) + + assertEquals("exp_1", pnPushList[0]["expiration"]) + assertEquals("collapse_1", pnPushList[0]["collapse_id"]) + assertEquals("v1", pnPushList[0]["version"]) + assertFalse(pnPushList[0].containsKey("targets")) + + assertEquals("exp_2", pnPushList[1]["expiration"]) + assertEquals("collapse_2", pnPushList[1]["collapse_id"]) + assertEquals("v2", pnPushList[1]["version"]) + assertFalse(pnPushList[1].containsKey("targets")) + + assertEquals("", pnPushList[2]["expiration"]) + assertFalse(pnPushList[2].containsKey("collapse_id")) + assertEquals("v3", pnPushList[2]["version"]) + assertTrue(pnPushList[2].containsKey("targets")) + + + val pnTargetsMap = pnPushList[2]["targets"] as List> + + assertEquals("development", pnTargetsMap[0]["environment"]) + assertEquals("topic_1", pnTargetsMap[0]["topic"]) + assertFalse(pnTargetsMap[0].containsKey("excludeDevices")) + + assertEquals("production", pnTargetsMap[1]["environment"]) + assertFalse(pnTargetsMap[1].containsKey("topic_2")) + assertFalse(pnTargetsMap[1].containsKey("excludeDevices")) + + assertEquals("production", pnTargetsMap[2]["environment"]) + assertEquals("topic_3", pnTargetsMap[2]["topic"]) + assertEquals("ex_1", (pnTargetsMap[2]["excluded_devices"] as List<*>?)!![0]) + assertEquals("ex_2", (pnTargetsMap[2]["excluded_devices"] as List<*>?)!![1]) + + assertEquals("production", pnTargetsMap[3]["environment"]) + assertFalse(pnTargetsMap[3].containsKey("topic_5")) + assertFalse(pnTargetsMap[3].containsKey("excludeDevices")) + + assertFalse(pnTargetsMap[4].containsKey("environment")) + assertEquals("topic_6", pnTargetsMap[4]["topic"]) + assertFalse(pnTargetsMap[4].containsKey("environment")) + } + + @Test + fun testApple_Aps_Empty() { + val pushPayloadHelper = PushPayloadHelper() + + val apnsPayload = APNSPayload() + apnsPayload.aps = APS() + pushPayloadHelper.apnsPayload = apnsPayload + + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testCommonPayload_Valid() { + val map = PushPayloadHelper().apply { + commonPayload = + mapOf( + "common_key_1" to 1, + "common_key_2" to "2", + "common_key_3" to true + ) + }.build() + + assertEquals(map["common_key_1"], 1) + assertEquals(map["common_key_2"], "2") + assertEquals(map["common_key_3"], true) + } + + @Test + fun testCommonPayload_Invalid() { + val pushPayloadHelper = PushPayloadHelper() + + pushPayloadHelper.commonPayload = mapOf() + + val map = pushPayloadHelper.build() + assertTrue(map.isEmpty()) + } + + @Test + fun testGoogle_Valid_1() { + val pushPayloadHelper = PushPayloadHelper() + + pushPayloadHelper.fcmPayload = FCMPayload().apply { + notification = FCMPayload.Notification().apply { + body = "Notification body" + image = null + title = "" + } + custom = mapOf("a" to "a", "b" to 1) + data = mapOf("data_1" to "a", "data_2" to 1) + } + + val map = pushPayloadHelper.build() + + val pnFcmMap = map["pn_gcm"] as Map<*, *> + assertNotNull(pnFcmMap) + + val pnFcmDataMap = pnFcmMap["data"] as Map<*, *> + val pnFcmNotificationsMap = pnFcmMap["notification"] as Map<*, *> + + assertNotNull(pnFcmDataMap) + assertNotNull(pnFcmNotificationsMap) + + assertEquals(pnFcmMap["a"], "a") + assertEquals(pnFcmMap["b"], 1) + assertEquals(pnFcmDataMap["data_1"], "a") + assertEquals(pnFcmDataMap["data_2"], 1) + assertEquals(pnFcmNotificationsMap["body"], "Notification body") + assertEquals(pnFcmNotificationsMap["image"], null) + assertEquals(pnFcmNotificationsMap["title"], "") + } + + @Test + fun testGoogle_Empty() { + val pushPayloadHelper = PushPayloadHelper() + val fcmPayload = FCMPayload() + pushPayloadHelper.fcmPayload = fcmPayload + val map = pushPayloadHelper.build() + val pnFcmMap = map["pn_gcm"] + assertNull(pnFcmMap) + } + + @Test + fun testGoogle_EmptyNotification() { + val pushPayloadHelper = PushPayloadHelper() + val notification = FCMPayload.Notification() + val customMap = HashMap() + customMap["key_1"] = "1" + customMap["key_2"] = 2 + val fcmPayload = FCMPayload() + fcmPayload.notification = notification + fcmPayload.custom = customMap + pushPayloadHelper.fcmPayload = fcmPayload + val map = pushPayloadHelper.build() + val pnFcmMap = map["pn_gcm"] as Map<*, *> + val pnFcmNotificationMap = pnFcmMap["notification"] + assertNull(pnFcmNotificationMap) + } + + @Test + fun testGoogle_EmptyData() { + val pushPayloadHelper = PushPayloadHelper() + val fcmPayload = FCMPayload() + val dataMap = HashMap() + fcmPayload.data = dataMap + pushPayloadHelper.fcmPayload = fcmPayload + val map = pushPayloadHelper.build() + val pnFcmMap = map["pn_gcm"] + assertNull(pnFcmMap) + } + + @Test + fun testGoogle_Valid_2() { + val pushPayloadHelper = PushPayloadHelper() + val fcmPayload = FCMPayload().apply { + data = mapOf( + "key_1" to "value_1", + "key_2" to 2, + "key_3" to true, + "key_4" to "" + ) + } + pushPayloadHelper.fcmPayload = fcmPayload + val map = pushPayloadHelper.build() + + val pnFcmMap = map["pn_gcm"] as Map<*, *> + val pnFcmDataMap = pnFcmMap["data"] as Map<*, *> + assertNotNull(pnFcmDataMap) + assertFalse(pnFcmDataMap.isEmpty()) + assertTrue(pnFcmDataMap.containsKey("key_1")) + assertTrue(pnFcmDataMap.containsKey("key_2")) + assertTrue(pnFcmDataMap.containsKey("key_3")) + assertTrue(pnFcmDataMap.containsKey("key_4")) + assertNotNull(pnFcmDataMap["key_1"]) + assertNotNull(pnFcmDataMap["key_2"]) + assertNotNull(pnFcmDataMap["key_3"]) + assertNotNull(pnFcmDataMap["key_4"]) + } + + @Test + fun testGoogle_Custom() { + val pushPayloadHelper = PushPayloadHelper() + pushPayloadHelper.fcmPayload = FCMPayload().apply { + custom = mapOf( + "key_1" to "value_1", + "key_2" to 2, + "key_3" to true, + "key_4" to "" + ) + } + val map = pushPayloadHelper.build() + + val pnFcmMap = map["pn_gcm"] as Map<*, *> + assertNotNull(pnFcmMap) + assertFalse(pnFcmMap.isEmpty()) + assertTrue(pnFcmMap.containsKey("key_1")) + assertTrue(pnFcmMap.containsKey("key_2")) + assertTrue(pnFcmMap.containsKey("key_3")) + assertTrue(pnFcmMap.containsKey("key_4")) + assertNotNull(pnFcmMap["key_1"]) + assertNotNull(pnFcmMap["key_2"]) + assertNotNull(pnFcmMap["key_3"]) + assertNotNull(pnFcmMap["key_4"]) + } + + @Test + fun testMicrosoft_Missing() { + val pushPayloadHelper = PushPayloadHelper() + + val mpnsPayload = MPNSPayload().apply { + backContent = "Back Content" + backTitle = "Back Title" + count = 1 + title = "Title" + type = "Type" + custom = mapOf( + "a" to "a", + "b" to 1, + "c" to "" + ) + } + pushPayloadHelper.mpnsPayload = mpnsPayload + val map = pushPayloadHelper.build() + + val pnMpnsMap = map["pn_mpns"] as HashMap<*, *> + assertNotNull(pnMpnsMap) + assertEquals(pnMpnsMap["back_content"], "Back Content") + assertEquals(pnMpnsMap["back_title"], "Back Title") + assertEquals(pnMpnsMap["count"], 1) + assertEquals(pnMpnsMap["title"], "Title") + assertEquals(pnMpnsMap["type"], "Type") + assertEquals(pnMpnsMap["a"], "a") + assertEquals(pnMpnsMap["b"], 1) + assertEquals(pnMpnsMap["c"], "") + } + + @Test + fun testMicrosoft_Valid() { + val pushPayloadHelper = PushPayloadHelper() + pushPayloadHelper.mpnsPayload = + MPNSPayload().apply { + backContent = "Back Content" + backTitle = "Back Title" + count = 1 + title = "Title" + type = "Type" + custom = mapOf( + "a" to "a", + "b" to 1, + "c" to "" + ) + } + + val map = pushPayloadHelper.build() + val pnMpnsMap = map["pn_mpns"] as Map<*, *> + + assertNotNull(pnMpnsMap) + assertEquals(pnMpnsMap["back_content"], "Back Content") + assertEquals(pnMpnsMap["back_title"], "Back Title") + assertEquals(pnMpnsMap["count"], 1) + assertEquals(pnMpnsMap["title"], "Title") + assertEquals(pnMpnsMap["type"], "Type") + assertEquals(pnMpnsMap["a"], "a") + assertEquals(pnMpnsMap["b"], 1) + assertEquals(pnMpnsMap["c"], "") + assertEquals(pnMpnsMap["d"], null) + } + + @Test + fun testMicrosoft_Empty() { + val pushPayloadHelper = PushPayloadHelper() + val mpnsPayload = MPNSPayload() + pushPayloadHelper.mpnsPayload = mpnsPayload + val map = pushPayloadHelper.build() + val pnMpnsMap = map["pn_mpns"] + assertNull(pnMpnsMap) + } + + @Test + fun testMicrosoft_Custom() { + val pushPayloadHelper = PushPayloadHelper() + pushPayloadHelper.mpnsPayload = MPNSPayload().apply { + backContent = "" + backTitle = "Back Title" + count = 1 + title = null + type = "Type" + custom = mapOf( + "a" to "a", + "b" to 1, + "c" to "" + ) + } + + val map = pushPayloadHelper.build() + + val pnMpnsMap = map["pn_mpns"] as Map<*, *> + assertNotNull(pnMpnsMap) + assertEquals("", pnMpnsMap["back_content"]) + assertEquals("Back Title", pnMpnsMap["back_title"]) + assertEquals(1, pnMpnsMap["count"]) + assertFalse(pnMpnsMap.containsKey("title")) + assertEquals("Type", pnMpnsMap["type"]) + assertEquals("a", pnMpnsMap["a"]) + assertEquals(1, pnMpnsMap["b"]) + assertEquals("", pnMpnsMap["c"]) + assertFalse(pnMpnsMap.containsKey("d")) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt new file mode 100644 index 000000000..4eb250eb1 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveAllPushChannelsForDeviceTest.kt @@ -0,0 +1,184 @@ +package com.pubnub.api.legacy.endpoints.push + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class RemoveAllPushChannelsForDeviceTest : BaseTest() { + + @Test + fun testAppleSuccessSyncRemoveAll() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("apns", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + + @Test + fun testFirebaseSuccessSyncRemoveAll() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("gcm", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testMicrosoftSuccessSyncRemoveAll() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.MPNS + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testApns2SuccessSyncRemoveAll() { + stubFor( + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + topic = "news" + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("development", requests[0].queryParameter("environment").firstValue()) + Assertions.assertEquals("news", requests[0].queryParameter("topic").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("type").isPresent) + } + + @Test + fun testIsAuthRequiredSuccessRemoveAll() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync() + + val requests = findAll(getRequestedFor(urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessRemoveAll() { + stubFor( + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice/remove")) + .willReturn(aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + val success = AtomicBoolean() + + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.async { _, status -> + Assertions.assertFalse(status.error) + Assertions.assertEquals(PNOperationType.PNRemoveAllPushNotificationsOperation, status.operation) + Assertions.assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertTrue(status.affectedChannels.isEmpty()) + Assertions.assertTrue(status.affectedChannelGroups.isEmpty()) + success.set(true) + } + + success.listen() + } + + @Test + fun testEmptySubscribeKeyRemoveAll() { + pubnub.configuration.subscribeKey = " " + + try { + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testNullPushTypeRemoveAll() { + try { + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + deviceId = "niceDevice" + }.sync()!! + } catch (e: PubNubException) { + assertPnException(PubNubError.PUSH_TYPE_MISSING, e) + } + } + + @Test + fun testNullDeviceIdRemoveAll() { + try { + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + pushType = PNPushType.FCM + }.sync()!! + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testEmptyDeviceIdRemoveAll() { + try { + pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + pushType = PNPushType.FCM + deviceId = " " + }.sync()!! + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } +} diff --git a/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt new file mode 100644 index 000000000..95e66d7e2 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/endpoints/push/RemoveChannelsFromPushTest.kt @@ -0,0 +1,229 @@ +package com.pubnub.api.legacy.endpoints.push + +import com.github.tomakehurst.wiremock.client.WireMock +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.concurrent.atomic.AtomicBoolean + +class RemoveChannelsFromPushTest : BaseTest() { + + @Test + fun testRemoveAppleSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("apns", requests[0].queryParameter("type").firstValue()) + Assertions.assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testRemoveFirebaseSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("gcm", requests[0].queryParameter("type").firstValue()) + Assertions.assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testRemoveMicrosoftSuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.MPNS + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("mpns", requests[0].queryParameter("type").firstValue()) + Assertions.assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("environment").isPresent) + Assertions.assertFalse(requests[0].queryParameter("topic").isPresent) + } + + @Test + fun testRemoveApns2SuccessSync() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.APNS2 + channels = listOf("chr1", "chr2", "chr3") + topic = "news" + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("chr1,chr2,chr3", requests[0].queryParameter("remove").firstValue()) + Assertions.assertEquals("development", requests[0].queryParameter("environment").firstValue()) + Assertions.assertEquals("news", requests[0].queryParameter("topic").firstValue()) + Assertions.assertFalse(requests[0].queryParameter("type").isPresent) + } + + @Test + fun testIsAuthRequiredSuccessRemove() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + pubnub.configuration.authKey = "myKey" + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("chr1", "chr2", "chr3") + topic = "news" + }.sync()!! + + val requests = WireMock.findAll(WireMock.getRequestedFor(WireMock.urlMatching("/.*"))) + Assertions.assertEquals(1, requests.size) + Assertions.assertEquals("myKey", requests[0].queryParameter("auth").firstValue()) + } + + @Test + fun testOperationTypeSuccessRemove() { + WireMock.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/niceDevice")) + .willReturn(WireMock.aResponse().withBody("[1, \"Modified Channels\"]")) + ) + + val success = AtomicBoolean() + + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("chr1", "chr2", "chr3") + }.async { _, status -> + Assertions.assertFalse(status.error) + Assertions.assertEquals(PNOperationType.PNRemovePushNotificationsFromChannelsOperation, status.operation) + Assertions.assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + Assertions.assertEquals(status.affectedChannels, listOf("chr1", "chr2", "chr3")) + Assertions.assertTrue(status.affectedChannelGroups.isEmpty()) + success.set(true) + } + + success.listen() + } + + @Test + fun testEmptySubscribeKeyRemove() { + pubnub.configuration.subscribeKey = " " + + try { + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + pushType = PNPushType.FCM + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + + @Test + fun testNullPushType() { + try { + pubnub.removePushNotificationsFromChannels().apply { + deviceId = "niceDevice" + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.PUSH_TYPE_MISSING, e) + } + } + + @Test + fun testNullDeviceId() { + try { + pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.FCM + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testEmptyDeviceId() { + try { + pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.FCM + deviceId = " " + channels = listOf("chr1", "chr2", "chr3") + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.DEVICE_ID_MISSING, e) + } + } + + @Test + fun testMissingChannels() { + try { + pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.FCM + deviceId = "niceDevice" + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + + @Test + fun testEmptyChannels() { + try { + pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.FCM + deviceId = "niceDevice" + channels = emptyList() + }.sync()!! + failTest() + } catch (e: PubNubException) { + assertPnException(PubNubError.CHANNEL_MISSING, e) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/managers/BasePathManagerTest.kt b/src/test/kotlin/com/pubnub/api/legacy/managers/BasePathManagerTest.kt new file mode 100644 index 000000000..af8d898d2 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/managers/BasePathManagerTest.kt @@ -0,0 +1,119 @@ +package com.pubnub.api.legacy.managers + +import com.pubnub.api.PNConfiguration +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.managers.BasePathManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class BasePathManagerTest : BaseTest() { + + lateinit var config: PNConfiguration + + override fun onBefore() { + config = PNConfiguration() + } + + @Test + fun stdOriginNotSecure() { + config.secure = false + val basePathManager = BasePathManager(config) + assertEquals("http://ps.pndsn.com", basePathManager.basePath()) + } + + @Test + fun stdOriginSecure() { + config.secure = true + val basePathManager = BasePathManager(config) + assertEquals("https://ps.pndsn.com", basePathManager.basePath()) + } + + @Test + fun customOriginNotSecure() { + config.origin = "custom.origin.com" + config.secure = false + val basePathManager = BasePathManager(config) + assertEquals("http://custom.origin.com", basePathManager.basePath()) + } + + @Test + fun customOriginSecure() { + config.origin = "custom.origin.com" + config.secure = true + val basePathManager = BasePathManager(config) + assertEquals("https://custom.origin.com", basePathManager.basePath()) + } + + @Test + fun customOriginNotSecureWithCacheBusting() { + config.origin = "custom.origin.com" + config.cacheBusting = true + config.secure = false + val basePathManager = BasePathManager(config) + assertEquals("http://custom.origin.com", basePathManager.basePath()) + } + + @Test + fun customOriginSecureWithCacheBusting() { + config.origin = "custom.origin.com" + config.secure = true + config.cacheBusting = true + val basePathManager = BasePathManager(config) + assertEquals("https://custom.origin.com", basePathManager.basePath()) + } + + @Test + fun cacheBustingNotSecure() { + config.cacheBusting = true + config.secure = false + val basePathManager = BasePathManager(config) + assertEquals("http://ps1.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps2.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps3.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps4.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps5.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps6.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps7.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps8.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps9.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps10.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps11.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps12.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps13.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps14.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps15.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps16.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps17.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps18.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps19.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps20.pndsn.com", basePathManager.basePath()) + assertEquals("http://ps1.pndsn.com", basePathManager.basePath()) + } + + @Test + fun cacheBustingSecure() { + config.cacheBusting = true + val basePathManager = BasePathManager(config) + assertEquals("https://ps1.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps2.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps3.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps4.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps5.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps6.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps7.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps8.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps9.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps10.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps11.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps12.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps13.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps14.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps15.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps16.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps17.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps18.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps19.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps20.pndsn.com", basePathManager.basePath()) + assertEquals("https://ps1.pndsn.com", basePathManager.basePath()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/managers/PublishSequenceManagerTest.kt b/src/test/kotlin/com/pubnub/api/legacy/managers/PublishSequenceManagerTest.kt new file mode 100644 index 000000000..f35b1fcae --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/managers/PublishSequenceManagerTest.kt @@ -0,0 +1,20 @@ +package com.pubnub.api.legacy.managers + +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.managers.PublishSequenceManager +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PublishSequenceManagerTest : BaseTest() { + + @Test + fun testSequenceManager() { + val publishSequenceManager = PublishSequenceManager(2) + + assertEquals(1, publishSequenceManager.nextSequence()) + assertEquals(2, publishSequenceManager.nextSequence()) + assertEquals(1, publishSequenceManager.nextSequence()) + assertEquals(2, publishSequenceManager.nextSequence()) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/legacy/managers/SubscriptionManagerTest.kt b/src/test/kotlin/com/pubnub/api/legacy/managers/SubscriptionManagerTest.kt new file mode 100644 index 000000000..e86fb845c --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/legacy/managers/SubscriptionManagerTest.kt @@ -0,0 +1,3240 @@ +package com.pubnub.api.legacy.managers + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.google.gson.reflect.TypeToken +import com.pubnub.api.* +import com.pubnub.api.callbacks.SubscribeCallback +import com.pubnub.api.enums.PNHeartbeatNotificationOptions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult +import org.awaitility.Awaitility +import org.hamcrest.Matchers +import org.hamcrest.core.IsEqual +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger + +class SubscriptionManagerTest : BaseTest() { + + @Test + fun testGetSubscribedChannels() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + val channels = pubnub.getSubscribedChannels() + assertTrue(channels.contains("ch1")) + assertTrue(channels.contains("ch2")) + } + + @Test + fun testGetSubscribedEmptyChannel() { + val gotMessages = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.subscribe().apply { + channels = listOf("") + }.execute() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + gotMessages.addAndGet(1) + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + gotMessages.addAndGet(1) + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + gotMessages.addAndGet(1) + } + + }) + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessages, IsEqual.equalTo(0)) + } + + @Test + fun testGetSubscribedEmptyChannelGroup() { + val gotMessages = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.subscribe().apply { + channelGroups = listOf("") + }.execute() + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + gotMessages.addAndGet(1) + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + gotMessages.addAndGet(1) + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + gotMessages.addAndGet(1) + } + + }) + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessages, IsEqual.equalTo(0)) + } + + @Test + fun testGetSubscribedChannelGroups() { + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.subscribe().apply { + channelGroups = listOf("cg1", "cg2") + }.execute() + + val groups: List = pubnub.getSubscribedChannelGroups() + assertTrue(groups.contains("cg1")) + assertTrue(groups.contains("cg2")) + } + + @Test + fun testPubNubUnsubscribeAll() { + stubFor( + get(urlPathMatching("/v2/subscribe/mySubscribeKey/.*")) + .willReturn(emptyJson()) + ) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1", "cg2") + withPresence = true + }.execute() + + var channels = pubnub.getSubscribedChannels() + assertTrue(channels.contains("ch1")) + assertTrue(channels.contains("ch2")) + + var groups = pubnub.getSubscribedChannelGroups() + assertTrue(groups.contains("cg1")) + assertTrue(groups.contains("cg2")) + + pubnub.unsubscribeAll() + + channels = pubnub.getSubscribedChannels() + assertEquals(0, channels.size) + + groups = pubnub.getSubscribedChannelGroups() + assertEquals(0, groups.size) + } + + @Test + fun testSubscribeBuilder() { + val gotStatus = AtomicInteger() + + val gotMessage = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + gotStatus.addAndGet(1) + } + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size > 0) + assertEquals("Message", pubnub.mapper.elementToString(pnMessageResult.message, "text")) + assertEquals("coolChannel", pnMessageResult.channel) + assertEquals(null, pnMessageResult.subscription) + assertEquals("Publisher-A", pnMessageResult.publisher) + gotMessage.set(true) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessage, IsEqual.equalTo(true)) + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribeDuplicateDisabledBuilder() { + val gotMessages = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + }, + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("5")) + .willReturn(emptyJson()) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + gotMessages.addAndGet(1) + } + + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessages, IsEqual.equalTo(2)) + } + + @Test + fun testSubscribeDuplicateBuilder() { + pubnub.configuration.dedupOnSubscribe = true + + val gotMessages = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + }, + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("5")) + .willReturn(emptyJson()) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + gotMessages.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessages, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribeDuplicateWithLimitBuilder() { + pubnub.configuration.dedupOnSubscribe = true + pubnub.configuration.maximumMessagesCacheSize = 1 + + val gotMessages = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message1" + }, + "b": "coolChannel" + }, + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message2" + }, + "b": "coolChannel" + }, + { + "a": "4", + "f": 0, + "i": "Publisher-A", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message1" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("5")) + .willReturn(notFound()) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + gotMessages.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(3, TimeUnit.SECONDS) + .untilAtomic(gotMessages, IsEqual.equalTo(3)) + } + + @Test + fun testQueueNotificationsBuilderNoThresholdSpecified() { + pubnub.configuration.requestMessageCountThreshold = null + + val gotStatus = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNRequestMessageCountExceededCategory) { + gotStatus.set(true) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(false)) + } + + @Test + fun testQueueNotificationsBuilderBelowThreshold() { + pubnub.configuration.requestMessageCountThreshold = 10 + + val gotStatus = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNRequestMessageCountExceededCategory) { + gotStatus.set(true) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(false)) + } + + @Test + fun testQueueNotificationsBuilderThresholdMatched() { + pubnub.configuration.requestMessageCountThreshold = 1 + + val gotStatus = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "o": { + "t": "14737141991877032", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannel" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNRequestMessageCountExceededCategory) { + gotStatus.set(true) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(true)) + } + + @Test + fun testQueueNotificationsBuilderThresholdExceeded() { + pubnub.configuration.requestMessageCountThreshold = 1 + + val gotStatus = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "m": [ + { + "a": "4", + "b": "coolChannel", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "f": 0, + "i": "Client-g5d4g", + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "o": { + "r": 2, + "t": "14737141991877032" + }, + "p": { + "r": 1, + "t": "14607577960925503" + } + }, + { + "a": "5", + "b": "coolChannel2", + "c": "coolChannel2", + "d": { + "text": "Message2" + }, + "f": 0, + "i": "Client-g5d4g", + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4g", + "o": { + "r": 2, + "t": "14737141991877033" + }, + "p": { + "r": 1, + "t": "14607577960925504" + } + } + ], + "t": { + "r": 1, + "t": "14607577960932487" + } + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNRequestMessageCountExceededCategory) { + gotStatus.set(true) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(true)) + } + + @Test + fun testSubscribeBuilderWithAccessManager403Error() { + val gotStatus = AtomicInteger() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withStatus(403).withBody( + """ + { + "message": "Forbidden", + "payload": { + "channels": [ + "ch1", + "ch2" + ], + "channel-groups": [ + ":cg1", + ":cg2" + ] + }, + "error": true, + "service": "Access Manager", + "status": 403 + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNAccessDeniedCategory) { + assertEquals(PNStatusCategory.PNAccessDeniedCategory, pnStatus.category) + assertEquals(listOf("ch1", "ch2"), pnStatus.affectedChannels) + assertEquals(listOf("cg1", "cg2"), pnStatus.affectedChannelGroups) + gotStatus.addAndGet(1) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(1)) + } + + @Test + fun testNamingSubscribeChannelGroupBuilder() { + val gotStatus = AtomicBoolean() + val gotMessage = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChannelGroup" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + assertEquals(2, pnStatus.affectedChannels.size) + gotStatus.set(true) + } + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size > 0) + assertEquals("Message", pubnub.mapper.elementToString(pnMessageResult.message, "text")) + assertEquals("coolChannel", pnMessageResult.channel) + assertEquals("coolChannelGroup", pnMessageResult.subscription) + gotMessage.set(true) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await().atMost(4, TimeUnit.SECONDS).untilTrue(gotMessage) + Awaitility.await().atMost(4, TimeUnit.SECONDS).untilTrue(gotStatus) + } + + @Test + fun testPresenceSubscribeBuilder() { + val gotStatus = AtomicInteger() + val gotMessage = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14614512228786519", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14614512228418349", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel-pnpres", + "d": { + "action": "join", + "timestamp": 1461451222, + "uuid": "4a6d5df7-e301-4e73-a7b7-6af9ab484eb0", + "occupancy": 1 + }, + "b": "coolChannel-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + gotStatus.addAndGet(1) + } + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size >= 1) + assertEquals("coolChannel", pnPresenceEventResult.channel) + assertEquals(null, pnPresenceEventResult.subscription) + gotMessage.set(true) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotMessage, IsEqual.equalTo(true)) + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(1)) + } + + @Test + fun testPresenceChannelGroupSubscribeBuilder() { + val gotStatus = AtomicInteger() + val gotMessage = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14614512228786519", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14614512228418349", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel-pnpres", + "d": { + "action": "join", + "timestamp": 1461451222, + "uuid": "4a6d5df7-e301-4e73-a7b7-6af9ab484eb0", + "occupancy": 1 + }, + "b": "coolChannelGroup-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + gotStatus.addAndGet(1) + } + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size >= 1) + assertEquals("coolChannel", pnPresenceEventResult.channel) + assertEquals("coolChannelGroup", pnPresenceEventResult.subscription) + gotMessage.set(true) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotMessage, IsEqual.equalTo(true)) + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(gotStatus, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribeSlidingBuilder() { + val gotMessage1 = AtomicBoolean() + val gotMessage2 = AtomicBoolean() + val gotMessage3 = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "3", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("3")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "10", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message3" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("10")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "20", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Message10" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("20")) + .willReturn(emptyJson()) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + when (pnMessageResult.message.asJsonObject["text"].asString) { + "Message" -> { + gotMessage1.set(true) + } + "Message3" -> { + gotMessage2.set(true) + } + "Message10" -> { + gotMessage3.set(true) + } + } + } + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAtomic( + gotMessage1, + IsEqual.equalTo(true) + ) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAtomic( + gotMessage2, + IsEqual.equalTo(true) + ) + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAtomic( + gotMessage3, + IsEqual.equalTo(true) + ) + } + + @Test + fun testSubscribeBuilderNumber() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": 10, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size >= 1) + assertEquals(10, pnMessageResult.message.asInt) + atomic.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribeBuilderWithMetadata() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14858178301085322", + "r": 7 + }, + "m": [ + { + "a": "4", + "f": 512, + "i": "02a7b822-220c-49b0-90c4-d9cbecc0fd85", + "s": 1, + "p": { + "t": "14858178301075219", + "r": 7 + }, + "k": "demo-36", + "c": "chTest", + "u": { + "status_update": { + "lat": 55.752023906250656, + "lon": 37.61749036080494, + "driver_id": 4722 + } + }, + "d": { + "City": "Goiania", + "Name": "Marcelo" + } + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size >= 1) + assertEquals( + """{"status_update":{"lat":55.752023906250656,"lon":37.61749036080494,"driver_id":4722}}""", + pnMessageResult.userMetadata.toString() + ) + atomic.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribeBuilderWithState() { + val subscribeHits = AtomicInteger(0) + val heartbeatHits = AtomicInteger(0) + val expectedPayload = PubNubUtil.urlDecode( + """%7B%22ch1%22%3A%5B%22p1%22%2C%22p2%22%5D%2C%22cg2%22%3A%5B%22p1%22%2C%22p2%22%5D%7D""" + ) + + val expectedMap = pubnub.mapper.fromJson?>( + expectedPayload, + object : TypeToken?>() {}.type + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn(emptyJson()) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/myUUID/data")) + .willReturn(emptyJson()) + ) + + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + val heartbeatRequests = findAll( + getRequestedFor( + urlMatching( + """/v2/presence/sub-key/${pubnub.configuration.subscribeKey}/channel/ch2,ch1/heartbeat.*""" + ) + ) + ) + val subscribeRequests = findAll( + getRequestedFor( + urlMatching( + """/v2/subscribe/${pubnub.configuration.subscribeKey}/ch2,ch1/.*""" + ) + ) + ) + for (request in subscribeRequests) { + val stateString = PubNubUtil.urlDecode(request.queryParameter("state").firstValue()) + var actualMap: HashMap? = null + try { + actualMap = pubnub.mapper.fromJson( + stateString, + object : TypeToken?>() {}.type + ) + } catch (e: PubNubException) { + e.printStackTrace() + } + if (actualMap != null && actualMap == expectedMap) { + subscribeHits.getAndAdd(1) + } + } + for (request in heartbeatRequests) { + if (!request.queryParams.containsKey("state")) { + heartbeatHits.getAndAdd(1) + } + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + channelGroups = listOf("cg1", "cg2") + }.execute() + + pubnub.setPresenceState().apply { + channels = listOf("ch1") + channelGroups = listOf("cg2") + state = listOf("p1", "p2") + }.async { _, _ -> } + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .until { subscribeHits.get() > 0 && heartbeatHits.get() > 0 } + } + + @Test + fun testSubscribeChannelGroupBuilder() { + val atomic = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + for (request in requests) { + val channelGroupQuery = request.queryParameter("channel-group") + if (channelGroupQuery != null && channelGroupQuery.firstValue() == "cg1,cg2") { + atomic.set(true) + } + } + } + + }) + + pubnub.subscribe().apply { + channelGroups = listOf("cg1", "cg2") + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilTrue(atomic) + } + + @Test + fun testSubscribeChannelGroupWithPresenceBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/,/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + for (request in requests) { + val channelGroups = request.queryParameter("channel-group") + .firstValue() + .split(",") + .toMutableList() + .sorted() + if ("cg1,cg1-pnpres,cg2,cg2-pnpres" == channelGroups.toCsv()) { + atomic.addAndGet(1) + } + } + } + + }) + + pubnub.subscribe().apply { + channelGroups = listOf("cg1", "cg2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribeWithFilterExpressionBuilder() { + val atomic = AtomicBoolean() + + pubnub.configuration.filterExpression = "much=filtering" + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("uuid", matching("myUUID")) + .withQueryParam("pnsdk", matching("PubNub-Kotlin/.*")) + .withQueryParam("filter-expr", matching("much=filtering")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .withQueryParam("tt", matching("5")) + .willReturn(notFound()) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = + findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size > 0) + atomic.set(true) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(atomic) + } + + @Test + fun testSubscribeWithEncryption() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14718972508742569", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 512, + "i": "ff374d0b-b866-40db-9ced-42d205bb808b", + "p": { + "t": "14718972508739738", + "r": 1 + }, + "k": "demo-36", + "c": "max_ch1", + "d": "6QoqmS9CnB3W9+I4mhmL7w==" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.configuration.cipherKey = "hello" + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size > 0) + assertEquals("hey", pubnub.mapper.elementToString(pnMessageResult.message, "text")) + atomic.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribeWithEncryptionPNOther() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14718972508742569", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 512, + "i": "ff374d0b-b866-40db-9ced-42d205bb808b", + "p": { + "t": "14718972508739738", + "r": 1 + }, + "k": "demo-36", + "c": "max_ch1", + "d": { + "pn_other": "6QoqmS9CnB3W9+I4mhmL7w==" + } + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.configuration.cipherKey = "hello" + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size > 0) + assertEquals( + "hey", + pnMessageResult.message.asJsonObject["pn_other"].asJsonObject["text"].asString + ) + atomic.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribePresenceBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = + findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + assertTrue(requests.size >= 1) + assertEquals("""{"text":"Enter Message Here"}""", pnMessageResult.message.toString()) + atomic.addAndGet(1) + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, Matchers.greaterThan(0)) + } + + @Test + fun testSubscribePresencePayloadHereNowRefreshDeltaBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14901247588021627", + "r": 2 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14901247587675704", + "r": 1 + }, + "k": "demo-36", + "c": "moon-interval-deltas-pnpres", + "d": { + "action": "interval", + "timestamp": 1490124758, + "occupancy": 2, + "here_now_refresh": true, + "join": [ + "2220E216-5A30-49AD-A89C-1E0B5AE26AD7", + "4262AE3F-3202-4487-BEE0-1A0D91307DEB" + ] + }, + "b": "moon-interval-deltas-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (atomic.get() == 0) { + assertEquals(true, pnPresenceEventResult.hereNowRefresh) + assertTrue(pnPresenceEventResult.occupancy!! == 2) + atomic.incrementAndGet() + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribePresencePayloadJoinDeltaBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14901247588021627", + "r": 2 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14901247587675704", + "r": 1 + }, + "k": "demo-36", + "c": "moon-interval-deltas-pnpres", + "d": { + "action": "interval", + "timestamp": 1490124758, + "occupancy": 2, + "join": [ + "2220E216-5A30-49AD-A89C-1E0B5AE26AD7", + "4262AE3F-3202-4487-BEE0-1A0D91307DEB" + ] + }, + "b": "moon-interval-deltas-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (atomic.get() == 0) { + val joinList: MutableList = ArrayList() + joinList.add("2220E216-5A30-49AD-A89C-1E0B5AE26AD7") + joinList.add("4262AE3F-3202-4487-BEE0-1A0D91307DEB") + assertEquals("interval", pnPresenceEventResult.event) + assertEquals(joinList, pnPresenceEventResult.join) + assertTrue(pnPresenceEventResult.occupancy!! == 2) + atomic.incrementAndGet() + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribePresencePayloadLeaveDeltaBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14901247588021627", + "r": 2 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14901247587675704", + "r": 1 + }, + "k": "demo-36", + "c": "moon-interval-deltas-pnpres", + "d": { + "action": "interval", + "timestamp": 1490124758, + "occupancy": 2, + "leave": [ + "2220E216-5A30-49AD-A89C-1E0B5AE26AD7", + "4262AE3F-3202-4487-BEE0-1A0D91307DEB" + ] + }, + "b": "moon-interval-deltas-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (atomic.get() == 0) { + val leaveList: MutableList = ArrayList() + leaveList.add("2220E216-5A30-49AD-A89C-1E0B5AE26AD7") + leaveList.add("4262AE3F-3202-4487-BEE0-1A0D91307DEB") + assertEquals("interval", pnPresenceEventResult.event) + assertEquals(leaveList, pnPresenceEventResult.leave) + assertTrue(pnPresenceEventResult.occupancy!! == 2) + atomic.incrementAndGet() + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribePresencePayloadTimeoutDeltaBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14901247588021627", + "r": 2 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14901247587675704", + "r": 1 + }, + "k": "demo-36", + "c": "moon-interval-deltas-pnpres", + "d": { + "action": "interval", + "timestamp": 1490124758, + "occupancy": 2, + "timeout": [ + "2220E216-5A30-49AD-A89C-1E0B5AE26AD7", + "4262AE3F-3202-4487-BEE0-1A0D91307DEB" + ] + }, + "b": "moon-interval-deltas-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (atomic.get() == 0) { + val timeoutList = listOf( + "2220E216-5A30-49AD-A89C-1E0B5AE26AD7", + "4262AE3F-3202-4487-BEE0-1A0D91307DEB" + ) + assertEquals("interval", pnPresenceEventResult.event) + assertEquals(timeoutList, pnPresenceEventResult.timeout) + assertTrue(pnPresenceEventResult.occupancy!! == 2) + atomic.incrementAndGet() + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribePresencePayloadBuilder() { + val atomic = AtomicInteger(0) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14614512228786519", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "p": { + "t": "14614512228418349", + "r": 2 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel-pnpres", + "d": { + "action": "join", + "timestamp": 1461451222, + "uuid": "4a6d5df7-e301-4e73-a7b7-6af9ab484eb0", + "occupancy": 1 + }, + "b": "coolChannel-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (atomic.get() == 0) { + assertEquals("join", pnPresenceEventResult.event) + assertEquals("4a6d5df7-e301-4e73-a7b7-6af9ab484eb0", pnPresenceEventResult.uuid) + assertTrue(pnPresenceEventResult.occupancy!! == 1) + assertTrue(pnPresenceEventResult.timestamp!! == 1461451222L) + atomic.incrementAndGet() + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(1)) + } + + @Test + fun testSubscribePresenceStateCallback() { + val atomic = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch10,ch10-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14637536741734954", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 512, + "p": { + "t": "14637536740940378", + "r": 1 + }, + "k": "demo-36", + "c": "ch10-pnpres", + "d": { + "action": "join", + "timestamp": 1463753674, + "uuid": "24c9bb19-1fcd-4c40-a6f1-522a8a1329ef", + "occupancy": 3 + }, + "b": "ch10-pnpres" + }, + { + "a": "4", + "f": 512, + "p": { + "t": "14637536741726901", + "r": 1 + }, + "k": "demo-36", + "c": "ch10-pnpres", + "d": { + "action": "state-change", + "timestamp": 1463753674, + "data": { + "state": "cool" + }, + "uuid": "24c9bb19-1fcd-4c40-a6f1-522a8a1329ef", + "occupancy": 3 + }, + "b": "ch10-pnpres" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + if (pnPresenceEventResult.event == "state-change") { + if (pnPresenceEventResult.state!!.asJsonObject.has("state") && + pnPresenceEventResult.state!!.asJsonObject.get("state").asString == "cool" + ) { + atomic.set(true) + } + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch10") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic( + atomic, IsEqual.equalTo(true) + ) + } + + @Test + fun testSubscribeRegionBuilder() { + val atomic = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 8 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) {} + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll(getRequestedFor(urlMatching("/v2/subscribe.*"))) + if (requests.size > 1) { + assertEquals("8", requests[1].queryParameter("tr").firstValue()) + atomic.set(true) + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(true)) + } + + @Test + fun testRemoveListener() { + stubFor( + get(urlPathMatching("/v2/subscribe/mySubscribeKey/.*")) + .willReturn(emptyJson()) + ) + + val atomic = AtomicInteger(0) + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + atomic.addAndGet(1) + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + atomic.addAndGet(1) + } + + override fun presence(pubnub: PubNub, pnPresenceEventResult: PNPresenceEventResult) { + atomic.addAndGet(1) + } + + } + + pubnub.addListener(sub1) + pubnub.removeListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(atomic, IsEqual.equalTo(0)) + } + + @Test + fun testUnsubscribe() { + val statusReceived = AtomicBoolean() + val messageReceived = AtomicBoolean() + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch2-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + pubnub.unsubscribe().apply { + channels = listOf("ch1") + }.execute() + } + val affectedChannels = pnStatus.affectedChannels + if (affectedChannels.size == 1 && pnStatus.operation == PNOperationType.PNUnsubscribeOperation) { + if (affectedChannels[0] == "ch1") { + statusReceived.set(true) + } + } + } + + override fun message(pubnub: PubNub, pnMessageResult: PNMessageResult) { + val requests = findAll( + getRequestedFor( + urlMatching("/v2/subscribe/mySubscribeKey/ch2,ch2-pnpres/0.*") + ) + ) + if (requests.isNotEmpty()) { + messageReceived.set(true) + } + } + + } + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic( + messageReceived, IsEqual.equalTo(true) + ) + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic( + statusReceived, IsEqual.equalTo(true) + ) + } + + @Test + fun testAllHeartbeats() { + val statusRecieved = AtomicBoolean() + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation && !pnStatus.error) { + statusRecieved.set(true) + } + } + + } + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(statusRecieved, IsEqual.equalTo(true)) + } + + @Test + fun testAllHeartbeatsViaPresence() { + val statusReceived = AtomicBoolean() + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation && !pnStatus.error) { + statusReceived.set(true) + } + } + + } + assertNotNull(sub1) + pubnub.addListener(sub1) + + pubnub.presence().apply { + channels = listOf("ch1", "ch2") + connected = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic( + statusReceived, IsEqual.equalTo(true) + ) + } + + @Test + fun testAllHeartbeatsLeaveViaPresence() { + val statusReceived = AtomicBoolean() + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1,ch2/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNUnsubscribeOperation && !pnStatus.error) { + statusReceived.set(true) + } + } + + } + + pubnub.addListener(sub1) + + pubnub.presence().apply { + channels = listOf("ch1", "ch2") + connected = false + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(statusReceived, IsEqual.equalTo(true)) + } + + @Test + fun testSuccessOnFailureVerbosityHeartbeats() { + val statusReceived = AtomicBoolean() + + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.FAILURES + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn(aResponse().withStatus(404).withBody("{}")) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .withQueryParam("tt", matching("5")) + .willReturn(notFound()) + ) + + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + statusReceived.set(true) + } + } + + } + + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(statusReceived, IsEqual.equalTo(true)) + } + + @Test + fun testFailedHeartbeats() { + val statusReceived = AtomicBoolean() + + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .withQueryParam("tt", matching("0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "5", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn(emptyJson().withStatus(403)) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .withQueryParam("tt", matching("5")) + .willReturn(notFound()) + ) + + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation && pnStatus.error) { + statusReceived.set(true) + } + } + + } + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(statusReceived, IsEqual.equalTo(true)) + } + + @Test + fun testSilencedHeartbeats() { + val statusReceived = AtomicBoolean() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.NONE + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [{ + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + } + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + }] + } + """.trimIndent() + ) + ) + ) + + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + statusReceived.set(true) + } + } + + } + + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(2, TimeUnit.SECONDS) + .untilAtomic(statusReceived, IsEqual.equalTo(false)) + } + + @Test + fun testFailedNoneHeartbeats() { + val statusReceived = AtomicBoolean() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.NONE + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2,ch1/heartbeat")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.operation != PNOperationType.PNHeartbeatOperation) { + statusReceived.set(true) + } + } + + } + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(4, TimeUnit.SECONDS) + .untilTrue(statusReceived) + } + + @Test + fun testHeartbeatsDisabled() { + val subscribeSuccess = AtomicBoolean() + val heartbeatFail = AtomicBoolean() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + + assertEquals(PNHeartbeatNotificationOptions.ALL, pubnub.configuration.heartbeatNotificationOptions) + assertEquals(300, pubnub.configuration.presenceTimeout) + assertEquals(0, pubnub.configuration.heartbeatInterval) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch1,ch1-pnpres/0")) + .willReturn( + aResponse() + .withBody( + """ + { + "t": { + "t": null, + "r": 12 + }, + "m": [] + } + """.trimIndent() + ) + .withStatus(200) + ) + ) + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse() + .withStatus(200) + .withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (!pnStatus.error) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation) { + subscribeSuccess.set(true) + } + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + heartbeatFail.set(true) + } + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .until { subscribeSuccess.get() && !heartbeatFail.get() } + } + + @Test + fun testHeartbeatsEnabled() { + val subscribeSuccess = AtomicBoolean() + val heartbeatSuccess = AtomicBoolean() + + pubnub.configuration.heartbeatNotificationOptions = PNHeartbeatNotificationOptions.ALL + + assertEquals(PNHeartbeatNotificationOptions.ALL, pubnub.configuration.heartbeatNotificationOptions) + assertEquals(300, pubnub.configuration.presenceTimeout) + assertEquals(0, pubnub.configuration.heartbeatInterval) + + pubnub.configuration.presenceTimeout = 20 + + assertEquals(20, pubnub.configuration.presenceTimeout) + assertEquals(9, pubnub.configuration.heartbeatInterval) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch1,ch1-pnpres/0")) + .willReturn( + aResponse() + .withBody( + """ + { + "t": { + "t": null, + "r": 12 + }, + "m": [] + } + """.trimIndent() + ) + .withStatus(200) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .willReturn( + aResponse() + .withStatus(200) + .withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + ) + ) + ) + + pubnub.addListener(object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (!pnStatus.error) { + if (pnStatus.operation == PNOperationType.PNSubscribeOperation) { + subscribeSuccess.set(true) + } + if (pnStatus.operation == PNOperationType.PNHeartbeatOperation) { + heartbeatSuccess.set(true) + } + } + } + + }) + + pubnub.subscribe().apply { + channels = listOf("ch1") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .until { subscribeSuccess.get() && heartbeatSuccess.get() } + } + + @Test + fun testMinimumPresenceValueNoInterval() { + pubnub.configuration.presenceTimeout = 10 + assertEquals(20, pubnub.configuration.presenceTimeout) + assertEquals(9, pubnub.configuration.heartbeatInterval) + } + + @Test + fun testMinimumPresenceValueWithInterval() { + pubnub.configuration.presenceTimeout = 20 + pubnub.configuration.heartbeatInterval = 50 + assertEquals(20, pubnub.configuration.presenceTimeout) + assertEquals(50, pubnub.configuration.heartbeatInterval) + } + + @Test + fun testUnsubscribeAll() { + val statusReceived = AtomicBoolean() + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch1,ch2-pnpres,ch1-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch2,ch2-pnpres/0")) + .willReturn( + aResponse().withBody( + """ + { + "t": { + "t": "14607577960932487", + "r": 1 + }, + "m": [ + { + "a": "4", + "f": 0, + "i": "Client-g5d4g", + "p": { + "t": "14607577960925503", + "r": 1 + }, + "k": "sub-c-4cec9f8e-01fa-11e6-8180-0619f8945a4f", + "c": "coolChannel", + "d": { + "text": "Enter Message Here" + }, + "b": "coolChan-bnel" + } + ] + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + stubFor( + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch2/leave")) + .willReturn( + aResponse().withBody( + """ + { + "status": 200, + "message": "OK", + "service": "Presence", + "action": "leave" + } + """.trimIndent() + ) + ) + ) + + val sub1: SubscribeCallback = object : SubscribeCallback() { + override fun status(pubnub: PubNub, pnStatus: PNStatus) { + if (pnStatus.category == PNStatusCategory.PNConnectedCategory) { + pubnub.unsubscribe().apply { + channels = listOf("ch1") + }.execute() + } + + val affectedChannels = pnStatus.affectedChannels + + if (affectedChannels.size == 1 && pnStatus.operation == PNOperationType.PNUnsubscribeOperation) { + if (affectedChannels[0] == "ch1") { + pubnub.unsubscribe().apply { + channels = listOf("ch2") + }.execute() + } + } + + if (affectedChannels.size == 1 && pnStatus.operation == PNOperationType.PNUnsubscribeOperation) { + if (affectedChannels[0] == "ch2") { + statusReceived.set(true) + } + } + } + + } + + pubnub.addListener(sub1) + + pubnub.subscribe().apply { + channels = listOf("ch1", "ch2") + withPresence = true + }.execute() + + Awaitility.await() + .atMost(4, TimeUnit.SECONDS) + .untilTrue(statusReceived) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/EndpointTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/EndpointTestSuite.kt new file mode 100644 index 000000000..46364be42 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/EndpointTestSuite.kt @@ -0,0 +1,407 @@ +package com.pubnub.api.suite + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.stubbing.StubMapping +import com.pubnub.api.* +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.legacy.BaseTest +import com.pubnub.api.models.consumer.PNStatus +import org.awaitility.Awaitility +import org.awaitility.Durations +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +typealias AsyncCheck = (status: PNStatus, result: T?) -> Unit + +abstract class EndpointTestSuite, R> : BaseTest() { + + private lateinit var expectedStub: StubMapping + + abstract fun telemetryParamName(): String + abstract fun pnOperation(): PNOperationType + abstract fun requiredKeys(): Int + abstract fun snippet(): T + abstract fun verifyResultExpectations(result: R) + abstract fun successfulResponseBody(): String + abstract fun unsuccessfulResponseBodyList(): List + abstract fun mappingBuilder(): MappingBuilder + abstract fun affectedChannelsAndGroups(): Pair, List> + + open fun optionalScenarioList(): List> = emptyList() + open fun voidResponse() = false + + override fun onBefore() { + super.onBefore() + expectedStub = stubFor(mappingBuilder().willReturn(aResponse().withBody(successfulResponseBody()))) + pubnub.configuration.includeInstanceIdentifier = false + pubnub.configuration.includeRequestIdentifier = false + } + + @Test + fun testTelemetryParameter() { + if (pnOperation() == PNOperationType.PNSubscribeOperation) + return + + stubTimeEndpoint() + + wireMockServer.removeStub(expectedStub) + + stubFor( + mappingBuilder() + .willReturn( + aResponse() + .withFixedDelay(50) + .withBody(successfulResponseBody()) + ) + ) + + lateinit var telemetryParamName: String + + snippet().await { _, status -> + assertFalse(status.error) + assertEquals(pnOperation(), status.operation) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + telemetryParamName = "l_${status.operation.queryParam!!}" + assertEquals(telemetryParamName(), telemetryParamName) + } + + Awaitility.await() + .pollInterval(Durations.FIVE_HUNDRED_MILLISECONDS) + .pollDelay(Durations.FIVE_HUNDRED_MILLISECONDS) + .atMost(Durations.FIVE_SECONDS) + .until { + val latch = CountDownLatch(1) + + pubnub.time().async { _, status -> + assertFalse(status.error) + assertNotNull(status.param(telemetryParamName)) + latch.countDown() + } + + latch.await(500, TimeUnit.MILLISECONDS) + } + } + + @Test + fun testSuccessAsync() { + snippet().await { result, status -> + // todo + // status.exception?.printStackTrace() + assertFalse(status.error) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + assertEquals(pnOperation(), status.operation) + assertEquals(status.affectedChannels, affectedChannelsAndGroups().first) + assertEquals(status.affectedChannelGroups, affectedChannelsAndGroups().second) + verifyResultExpectations(result!!) + } + } + + @Test + fun testSuccessSync() { + runSync() + } + + @Test + fun testKeys() { + testSubscribeKey() + testPublishKey() + testAuthKeySync() + testAuthKeyAsync() + testSecretKey() + } + + @Test + fun testUnsuccessfulResponsesSync() { + wireMockServer.removeStub(expectedStub) + + unsuccessfulResponseBodyList().forEach { + val stub = stubFor(mappingBuilder().willReturn(aResponse().withBody(it))) + try { + testSuccessSync() + failTest() + } catch (e: Exception) { + e.printStackTrace() + assertPnException(PubNubError.PARSING_ERROR, e) + } + wireMockServer.removeStub(stub) + } + } + + @Test + fun testUnsuccessfulResponsesAsync() { + wireMockServer.removeStub(expectedStub) + + unsuccessfulResponseBodyList().forEach { + val stub = stubFor(mappingBuilder().willReturn(aResponse().withBody(it))) + snippet().await { _, status -> + assertTrue(status.error) + assertPnException(PubNubError.PARSING_ERROR, status) + } + wireMockServer.removeStub(stub) + } + } + + @Test + fun testUsualWrongResponses() { + wireMockServer.removeStub(expectedStub) + + val map = hashMapOf( + "empty_json" to emptyJson(), + "no_content" to noContent(), + "malformed" to aResponse().withBody("{") + ) + + if (pnOperation() == PNOperationType.PNSubscribeOperation) + map.remove("empty_json") + + map.forEach { + val stub = stubFor(mappingBuilder().willReturn(it.value)) + try { + val result = snippet().sync() + if (voidResponse()) { + assertNotNull(result) + } else { + failTest(it.key) + } + } catch (e: Exception) { + if (voidResponse()) { + failTest(it.key) + } + assertPnException(PubNubError.PARSING_ERROR, e) + } + + snippet().await { result, status -> + assertEquals(!voidResponse(), status.error) + + if (!voidResponse()) { + assertNull(result) + assertPnException(PubNubError.PARSING_ERROR, status) + assertEquals(PNStatusCategory.PNMalformedResponseCategory, status.category) + } else { + assertNotNull(result) + assertEquals(PNStatusCategory.PNAcknowledgmentCategory, status.category) + } + + } + + wireMockServer.removeStub(stub) + } + } + + @Test + fun testOptionalResponsesSync() { + wireMockServer.removeStub(expectedStub) + + optionalScenarioList().forEach { + val stub = stubFor(mappingBuilder().willReturn(it.build())) + + try { + snippet().sync()!! + if (it.result == Result.FAIL) { + failTest() + } + } catch (e: Exception) { + if (it.result == Result.FAIL) { + it.pnError?.let { pubNubError -> + assertPnException(pubNubError, e) + } + } + } + + wireMockServer.removeStub(stub) + } + } + + @Test + fun testOptionalResponsesAsync() { + wireMockServer.removeStub(expectedStub) + + optionalScenarioList().forEach { + val stub = stubFor(mappingBuilder().willReturn(it.build())) + + snippet().await { result, status -> + it.additionalChecks.invoke(status, result) + if (it.result == Result.SUCCESS) { + assertFalse(status.error) + result!! + } else if (it.result == Result.FAIL) { + assertTrue(status.error) + assertNull(result) + it.pnError?.let { pubNubError -> + assertPnException(pubNubError, status) + } + } + } + + wireMockServer.removeStub(stub) + } + } + + @Test + fun testUrlEncoding() { + snippet().apply { + queryParam = getSpecialCharsMap().map { + it.name to it.regular + }.toMap() + }.await { result, status -> + assertFalse(status.error) + assertNotNull(result) + + getSpecialCharsMap().shuffled().forEach { + val encodedParam = status.encodedParam(it.name) + assertEquals(it.encoded, encodedParam) + } + } + } + + private fun testSubscribeKey() { + pubnub.configuration.subscribeKey = " " + try { + testSuccessSync() + if (requiredKeys().contains(SUB)) { + failTest() + } + } catch (e: Exception) { + if (requiredKeys().contains(SUB)) { + assertPnException(PubNubError.SUBSCRIBE_KEY_MISSING, e) + } + } + pubnub.configuration.subscribeKey = "mySubscribeKey" + } + + private fun testPublishKey() { + pubnub.configuration.publishKey = " " + try { + testSuccessSync() + if (requiredKeys().contains(PUB)) { + failTest() + } + } catch (e: Exception) { + if (requiredKeys().contains(PUB)) { + assertPnException(PubNubError.PUBLISH_KEY_MISSING, e) + } + } + pubnub.configuration.publishKey = "myPublishKey" + } + + private fun testAuthKeySync() { + pubnub.configuration.authKey = "someAuthKey" + testSuccessSync() + + val requests = findAll(anyRequestedFor(mappingBuilder().build().request.urlMatcher)) + + if (requiredKeys().contains(AUTH)) { + assertEquals(pubnub.configuration.authKey, requests.last().queryParameter("auth").firstValue()) + } else { + requests.forEach { + assertFalse(it.queryParams.containsKey("auth")) + } + } + } + + private fun testAuthKeyAsync() { + pubnub.configuration.authKey = "someAuthKey" + + snippet().await { _, status -> + assertFalse(status.error) + + if (requiredKeys().contains(AUTH)) { + assertEquals(pubnub.configuration.authKey, status.param("auth")) + } else { + assertNull(status.param("auth")) + } + } + } + + private fun testSecretKey() { + pubnub.configuration.secretKey = " " + try { + testSuccessSync() + if (requiredKeys().contains(SEC)) { + failTest() + } + } catch (e: Exception) { + if (requiredKeys().contains(SEC)) { + assertPnException(PubNubError.SECRET_KEY_MISSING, e) + } + } + pubnub.configuration.secretKey = "mySecretKey" + } + + private fun runSync() { + val result = snippet().sync()!! + verifyResultExpectations(result) + } + + private fun stubTimeEndpoint() { + stubFor( + get(urlMatching("/time/0.*")) + .willReturn( + aResponse() + .withBody("[1000]") + ) + ) + } +} + +private fun extractKeys(n: Int): List { + val keys = mutableListOf() + var n = n + while (n > 0) { + val power = { + var res = 0 + for (i in n downTo 1) { + if (i and i - 1 == 0) { + res = i + break + } + } + res + }.invoke() + keys.add(power) + n -= power + } + return keys +} + +val SUB = 0b001 +val PUB = 0b010 +val AUTH = 0b100 +val SEC = 0b1000 + +private fun Int.contains(sub: Int): Boolean { + return extractKeys(this).contains(sub) +} + +fun Endpoint.await(function: (result: Output?, status: PNStatus) -> Unit) { + val success = AtomicBoolean() + async { result, status -> + function.invoke(result, status) + success.set(true) + } + success.listen() +} + +class OptionalScenario { + var responseBuilder: ResponseDefinitionBuilder.() -> ResponseDefinitionBuilder = { this } + + var additionalChecks: AsyncCheck = { _: PNStatus, _: R? -> } + var result: Result = Result.SUCCESS + var pnError: PubNubError? = null + + internal fun build(): ResponseDefinitionBuilder { + return aResponse().responseBuilder() + } +} + +enum class Result { + SUCCESS, + FAIL +} + diff --git a/src/test/kotlin/com/pubnub/api/suite/TimeTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/TimeTestSuite.kt new file mode 100644 index 000000000..cef66e7c8 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/TimeTestSuite.kt @@ -0,0 +1,57 @@ +package com.pubnub.api.suite + +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.Time +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.PNTimeResult +import org.junit.jupiter.api.Assertions.* + +class TimeTestSuite : EndpointTestSuite() { + override fun telemetryParamName() = "l_time" + + override fun pnOperation() = PNOperationType.PNTimeOperation + + override fun requiredKeys() = 0 + + override fun snippet(): Time { + return pubnub.time() + } + + override fun verifyResultExpectations(result: PNTimeResult) { + assertEquals(1000, result.timetoken) + } + + override fun successfulResponseBody() = """[1000]""" + + override fun unsuccessfulResponseBodyList() = listOf( + """{}""", + """[false]""" + ) + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("[wrong]") } + result = Result.FAIL + additionalChecks = { status: PNStatus, result: PNTimeResult? -> + assertTrue(status.error) + assertNull(result) + } + }, + OptionalScenario().apply { + responseBuilder = { withBody("[123]") } + additionalChecks = { status: PNStatus, result: PNTimeResult? -> + assertFalse(status.error) + assertEquals(123, result!!.timetoken) + } + } + ) + } + + override fun mappingBuilder() = get(urlPathEqualTo("/time/0")) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/channel_groups/AddChannelChannelGroupTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/channel_groups/AddChannelChannelGroupTestSuite.kt new file mode 100644 index 000000000..21e3b0d1b --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/channel_groups/AddChannelChannelGroupTestSuite.kt @@ -0,0 +1,51 @@ +package com.pubnub.api.suite.channel_groups + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.channel_groups.AddChannelChannelGroup +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAddChannelResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class AddChannelChannelGroupTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_cg" + + override fun pnOperation() = PNOperationType.PNAddChannelsToGroupOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): AddChannelChannelGroup { + return pubnub.addChannelsToChannelGroup().apply { + channelGroup = "cg1" + channels = listOf("ch1", "ch2") + } + } + + override fun verifyResultExpectations(result: PNChannelGroupsAddChannelResult) { + + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "service": "channel-registry", + "error": false + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/cg1")) + .withQueryParam("add", equalTo("ch1,ch2")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to listOf("cg1") + + override fun voidResponse() = true + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/channel_groups/AllChannelsChannelGroupTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/channel_groups/AllChannelsChannelGroupTestSuite.kt new file mode 100644 index 000000000..d39ef7574 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/channel_groups/AllChannelsChannelGroupTestSuite.kt @@ -0,0 +1,101 @@ +package com.pubnub.api.suite.channel_groups + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.channel_groups.AllChannelsChannelGroup +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsAllChannelsResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class AllChannelsChannelGroupTestSuite : + EndpointTestSuite() { + + override fun telemetryParamName() = "l_cg" + + override fun pnOperation() = PNOperationType.PNChannelsForGroupOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): AllChannelsChannelGroup { + return pubnub.listChannelsForChannelGroup().apply { + channelGroup = "cg1" + } + } + + override fun verifyResultExpectations(result: PNChannelGroupsAllChannelsResult) { + assertEquals(listOf("ch1", "ch2"), result.channels) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "payload": { + "channels": [ + "ch1", + "ch2" + ], + "group": "cg1" + }, + "service": "channel-registry", + "error": false + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"payload":{"channels":null,"group":null}}""", + """{"payload":{"channels":null}}""", + """{"payload":{}}""", + """{"payload":null}""" + ) + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/cg1")) + } + + override fun affectedChannelsAndGroups() = emptyList() to listOf("cg1") + + override fun optionalScenarioList() = listOf( + OptionalScenario().apply { + responseBuilder = { + withBody( + """ + { + "payload": { + "channels": [], + "group": "cg1" + } + } + """.trimIndent() + ) + } + additionalChecks = { status: PNStatus, result: PNChannelGroupsAllChannelsResult? -> + assertFalse(status.error) + assertTrue(result!!.channels.isEmpty()) + } + }, + OptionalScenario().apply { + responseBuilder = { + withBody( + """ + { + "payload": { + "group": "cg1" + } + } + """.trimIndent() + ) + } + pnError = PubNubError.PARSING_ERROR + result = Result.FAIL + additionalChecks = { status: PNStatus, result: PNChannelGroupsAllChannelsResult? -> + assertTrue(status.error) + } + } + + ) + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/channel_groups/DeleteChannelGroupTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/channel_groups/DeleteChannelGroupTestSuite.kt new file mode 100644 index 000000000..fb8a6a51e --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/channel_groups/DeleteChannelGroupTestSuite.kt @@ -0,0 +1,50 @@ +package com.pubnub.api.suite.channel_groups + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.channel_groups.DeleteChannelGroup +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsDeleteGroupResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class DeleteChannelGroupTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_cg" + + override fun pnOperation() = PNOperationType.PNRemoveGroupOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): DeleteChannelGroup { + return pubnub.deleteChannelGroup().apply { + channelGroup = "cg1" + } + } + + override fun verifyResultExpectations(result: PNChannelGroupsDeleteGroupResult) { + + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "service": "channel-registry", + "error": false + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/cg1/remove")) + } + + override fun affectedChannelsAndGroups() = emptyList() to listOf("cg1") + + override fun voidResponse() = true + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/channel_groups/ListAllChannelGroupTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/channel_groups/ListAllChannelGroupTestSuite.kt new file mode 100644 index 000000000..52c1924cb --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/channel_groups/ListAllChannelGroupTestSuite.kt @@ -0,0 +1,58 @@ +package com.pubnub.api.suite.channel_groups + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.channel_groups.ListAllChannelGroup +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsListAllResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class ListAllChannelGroupTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_cg" + + override fun pnOperation() = PNOperationType.PNChannelGroupsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): ListAllChannelGroup { + return pubnub.listAllChannelGroups() + } + + override fun verifyResultExpectations(result: PNChannelGroupsListAllResult) { + assertEquals(listOf("cg1", "cg2"), result.groups) + } + + override fun successfulResponseBody() = """{"payload":{"groups":["cg1","cg2"]}}""" + + override fun unsuccessfulResponseBodyList() = listOf( + """{"payload":{"groups":null}}""", + """{"payload":{"groups":{}}}""", + """{"payload":{}}""", + """{"payload":null}""" + ) + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group")) + } + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { + withBody("""{"payload":{"groups":[]}}""") + } + additionalChecks = { status: PNStatus, result: PNChannelGroupsListAllResult? -> + assertFalse(status.error) + assertTrue(result!!.groups.isEmpty()) + } + result = Result.SUCCESS + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/channel_groups/RemoveChannelChannelGroupTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/channel_groups/RemoveChannelChannelGroupTestSuite.kt new file mode 100644 index 000000000..571acc927 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/channel_groups/RemoveChannelChannelGroupTestSuite.kt @@ -0,0 +1,57 @@ +package com.pubnub.api.suite.channel_groups + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.channel_groups.RemoveChannelChannelGroup +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.channel_group.PNChannelGroupsRemoveChannelResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue + +class RemoveChannelChannelGroupTestSuite : + EndpointTestSuite() { + + override fun telemetryParamName() = "l_cg" + + override fun pnOperation() = PNOperationType.PNRemoveChannelsFromGroupOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveChannelChannelGroup { + return pubnub.removeChannelsFromChannelGroup().apply { + channelGroup = "cg1" + channels = listOf("ch1", "ch2") + } + } + + override fun verifyResultExpectations(result: PNChannelGroupsRemoveChannelResult) {} + + override fun successfulResponseBody() = """{"payload":{"groups":["cg1","cg2"]}}""" + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v1/channel-registration/sub-key/mySubscribeKey/channel-group/cg1")) + .withQueryParam("remove", equalTo("ch1,ch2")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to listOf("cg1") + + override fun voidResponse() = true + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("").withStatus(400) } + result = Result.FAIL + additionalChecks = { pnStatus: PNStatus, result: PNChannelGroupsRemoveChannelResult? -> + assertTrue(pnStatus.error) + assertNull(result) + } + } + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/grant/GrantTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/grant/GrantTestSuite.kt new file mode 100644 index 000000000..0c0632fff --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/grant/GrantTestSuite.kt @@ -0,0 +1,76 @@ +package com.pubnub.api.suite.grant + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.access.Grant +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.access_manager.PNAccessManagerGrantResult +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SEC +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class GrantTestSuite : EndpointTestSuite() { + + override fun onBefore() { + super.onBefore() + pubnub.configuration.secretKey = "mySecretKey" + } + + override fun telemetryParamName() = "l_pam"; + + override fun pnOperation() = PNOperationType.PNAccessManagerGrant + + override fun requiredKeys() = SUB + SEC + + override fun snippet(): Grant { + return pubnub.grant().apply { + + } + } + + override fun verifyResultExpectations(result: PNAccessManagerGrantResult) { + assertEquals("mySubscribeKey", result.subscribeKey) + assertEquals(0, result.channels.size) + assertEquals(0, result.channelGroups.size) + assertEquals("subkey", result.level) + assertEquals(1, result.ttl) + } + + override fun successfulResponseBody() = """ + { + "message": "Success", + "payload": { + "level": "subkey", + "subscribe_key": "mySubscribeKey", + "ttl": 1, + "r": 0, + "w": 0, + "m": 0, + "d": 0 + }, + "service": "Access Manager", + "status": 200 + } + """.trimIndent() + + + override fun unsuccessfulResponseBodyList() = listOf( + """{"payload":{}}""", + """{"payload":null}""" + ) + + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v2/auth/grant/sub-key/mySubscribeKey")) + .withQueryParam("r", equalTo("0")) + .withQueryParam("w", equalTo("0")) + .withQueryParam("d", equalTo("0")) + .withQueryParam("m", equalTo("0")) + .withQueryParam("ttl", equalTo("-1")) + .withQueryParam("signature", matching(".*")) + } + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/DeleteMessagesTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/DeleteMessagesTestSuite.kt new file mode 100644 index 000000000..ca2272175 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/DeleteMessagesTestSuite.kt @@ -0,0 +1,50 @@ +package com.pubnub.api.suite.history + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.DeleteMessages +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNDeleteMessagesResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class DeleteMessagesTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_hist" + + override fun pnOperation() = PNOperationType.PNDeleteMessagesOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): DeleteMessages { + return pubnub.deleteMessages().apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: PNDeleteMessagesResult) { + + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "error": false, + "error_message": "" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return delete(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/ch1")) + .withQueryParam("start", absent()) + .withQueryParam("end", absent()) + } + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun voidResponse() = true + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/counts/MessageCountsTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/counts/MessageCountsTestSuite.kt new file mode 100644 index 000000000..2e006eb3c --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/counts/MessageCountsTestSuite.kt @@ -0,0 +1,53 @@ +package com.pubnub.api.suite.history.counts + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.MessageCounts +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.history.PNMessageCountResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class MessageCountsTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_mc" + + override fun pnOperation() = PNOperationType.PNMessageCountOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): MessageCounts { + return pubnub.messageCounts().apply { + channels = listOf("ch1") + channelsTimetoken = listOf(1588284000000) + } + } + + override fun verifyResultExpectations(result: PNMessageCountResult) { + assertEquals(1, result.channels.keys.size) + assertEquals(5L, result.channels["ch1"]) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "error": false, + "error_message": "", + "channels": { + "ch1": 5 + } + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/message-counts/ch1")) + .withQueryParam("timetoken", equalTo("1588284000000")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryMetaTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryMetaTestSuite.kt new file mode 100644 index 000000000..03c3de5c4 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryMetaTestSuite.kt @@ -0,0 +1,100 @@ +package com.pubnub.api.suite.history.v2 + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.History +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.history.PNHistoryResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class HistoryMetaTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_hist" + + override fun pnOperation() = PNOperationType.PNHistoryOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): History { + return pubnub.history().apply { + channel = "ch1" + includeMeta = true + includeTimetoken = true + } + } + + override fun verifyResultExpectations(result: PNHistoryResult) { + assertEquals(100L, result.startTimetoken) + assertEquals(200L, result.endTimetoken) + assertEquals(2, result.messages.size) + assertEquals("msg1", result.messages[0].entry.asString) + assertEquals("msg2", result.messages[1].entry.asString) + assertTrue(result.messages[0].meta!!.asString.isBlank()) + assertEquals( + mapOf("color" to "red"), Gson().fromJson( + result.messages[1].meta!!.asJsonObject, + object : TypeToken?>() {}.type + ) + ) + assertEquals(100L, result.messages[0].timetoken) + assertEquals(200L, result.messages[1].timetoken) + } + + override fun successfulResponseBody() = """ + [ + [ + { + "message": "msg1", + "timetoken": 100, + "meta": "" + }, + { + "message": "msg2", + "timetoken": 200, + "meta": { + "color": "red" + } + } + ], + 100, + 200 + ] + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + "[]", + "[{}]" + ) + + override fun mappingBuilder(): MappingBuilder { + return get( + urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/ch1") + ) + .withQueryParam("include_token", equalTo("true")) + .withQueryParam("count", equalTo("100")) + .withQueryParam("include_meta", equalTo("true")) + .withQueryParam("reverse", equalTo("false")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList() = listOf( + OptionalScenario().apply { + responseBuilder = { + withBody("""["First Element Not An Array",0,0]""") + } + additionalChecks = { status: PNStatus, _: PNHistoryResult? -> + assertTrue(status.error) + assertEquals(status.exception!!.errorMessage, "History is disabled") + } + result = Result.FAIL + pnError = PubNubError.HTTP_ERROR + } + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryTestSuite.kt new file mode 100644 index 000000000..6dc5ef3e4 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/v2/HistoryTestSuite.kt @@ -0,0 +1,71 @@ +package com.pubnub.api.suite.history.v2 + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.History +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.history.PNHistoryResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class HistoryTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_hist" + + override fun pnOperation() = PNOperationType.PNHistoryOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): History { + return pubnub.history().apply { + channel = "ch1" + } + } + + override fun verifyResultExpectations(result: PNHistoryResult) { + assertEquals(100L, result.startTimetoken) + assertEquals(200L, result.endTimetoken) + assertEquals(2, result.messages.size) + assertEquals("msg1", result.messages[0].entry.asString) + assertEquals("msg2", result.messages[1].entry.asString) + assertNull(result.messages[0].meta) + assertNull(result.messages[1].meta) + assertNull(result.messages[0].timetoken) + assertNull(result.messages[1].timetoken) + } + + override fun successfulResponseBody() = """[["msg1","msg2"],100,200]""" + + override fun unsuccessfulResponseBodyList() = listOf( + "[]", + "[{}]" + ) + + override fun mappingBuilder(): MappingBuilder { + return get( + urlPathEqualTo("/v2/history/sub-key/mySubscribeKey/channel/ch1") + ) + .withQueryParam("include_token", equalTo("false")) + .withQueryParam("count", equalTo("100")) + .withQueryParam("include_meta", equalTo("false")) + .withQueryParam("reverse", equalTo("false")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList() = listOf( + OptionalScenario().apply { + responseBuilder = { + withBody("""["First Element Not An Array",0,0]""") + } + additionalChecks = { status: PNStatus, _: PNHistoryResult? -> + assertTrue(status.error) + assertEquals(status.exception!!.errorMessage, "History is disabled") + } + result = Result.FAIL + pnError = PubNubError.HTTP_ERROR + } + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesMetaActionsTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesMetaActionsTestSuite.kt new file mode 100644 index 000000000..04edc00e7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesMetaActionsTestSuite.kt @@ -0,0 +1,124 @@ +package com.pubnub.api.suite.history.v3 + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.google.gson.JsonObject +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.FetchMessages +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.history.PNFetchMessagesResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class FetchMessagesMetaActionsTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_hist" + + override fun pnOperation() = PNOperationType.PNFetchMessagesOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): FetchMessages { + return pubnub.fetchMessages().apply { + channels = listOf("ch1") + includeMeta = true + includeMessageActions = true + } + } + + override fun verifyResultExpectations(result: PNFetchMessagesResult) { + assertEquals(1, result.channels.size) + assertTrue(result.channels.containsKey("ch1")) + assertEquals("bar", result.channels["ch1"]!![0].message.asString) + assertEquals( + JsonObject().apply { addProperty("color", "red") }, + result.channels["ch1"]!![0].meta + ) + assertEquals(100, result.channels["ch1"]!![0].timetoken) + + val actions = result.channels["ch1"]!![0].actions + assertEquals(1, actions!!.keys.size) + assertEquals(1, actions["reaction"]!!["smile"]!!.size) + assertEquals("publishersUuid", actions["reaction"]!!["smile"]!![0].uuid) + assertEquals("200", actions["reaction"]!!["smile"]!![0].actionTimetoken) + } + + override fun successfulResponseBody() = """ + { + "channels": { + "ch1": [ + { + "message": "bar", + "meta": { + "color": "red" + }, + "timetoken": 100, + "actions": { + "reaction": { + "smile": [ + { + "uuid": "publishersUuid", + "actionTimetoken": "200" + } + ] + } + } + } + ] + } + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """ + { + "channels": { + "ch1": [ + { + "timetoken": 100 + } + ] + } + } + """.trimIndent(), + """ + { + "channels": { + "ch1": [ + { + "entry": "hello", + "timetoken": 100 + } + ], + "ch2": [], + "ch3": null + } + } + """.trimIndent() + ) + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v3/history-with-actions/sub-key/mySubscribeKey/channel/ch1")) + .withQueryParam("max", equalTo("25")) + .withQueryParam("include_meta", equalTo("true")) + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("""{"channels":{"ch3":null}}""") } + result = Result.FAIL + pnError = PubNubError.PARSING_ERROR + additionalChecks = { status: PNStatus, result: PNFetchMessagesResult? -> + assertTrue(status.error) + assertNull(result) + assertEquals(PNStatusCategory.PNMalformedResponseCategory, status.category) + } + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesTestSuite.kt new file mode 100644 index 000000000..320b15716 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/history/v3/FetchMessagesTestSuite.kt @@ -0,0 +1,99 @@ +package com.pubnub.api.suite.history.v3 + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.FetchMessages +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNStatusCategory +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.history.PNFetchMessagesResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class FetchMessagesTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_hist" + + override fun pnOperation() = PNOperationType.PNFetchMessagesOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): FetchMessages { + return pubnub.fetchMessages().apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: PNFetchMessagesResult) { + assertEquals(1, result.channels.size) + assertTrue(result.channels.containsKey("ch1")) + assertEquals("hello", result.channels["ch1"]!![0].message.asString) + assertEquals(100, result.channels["ch1"]!![0].timetoken) + assertNull(result.channels["ch1"]!![0].meta) + } + + override fun successfulResponseBody() = """ + { + "channels": { + "ch1": [ + { + "message": "hello", + "timetoken": 100 + } + ] + } + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """ + { + "channels": { + "ch1": [ + { + "timetoken": 100 + } + ] + } + } + """.trimIndent(), + """ + { + "channels": { + "ch1": [ + { + "entry": "hello", + "timetoken": 100 + } + ], + "ch2": [], + "ch3": null + } + } + """.trimIndent() + ) + + override fun mappingBuilder(): MappingBuilder { + return get(urlPathEqualTo("/v3/history/sub-key/mySubscribeKey/channel/ch1")) + .withQueryParam("max", equalTo("1")) + .withQueryParam("include_meta", absent()) + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("""{"channels":{"ch3":null}}""") } + result = Result.FAIL + pnError = PubNubError.PARSING_ERROR + additionalChecks = { status: PNStatus, result: PNFetchMessagesResult? -> + assertTrue(status.error) + assertNull(result) + assertEquals(PNStatusCategory.PNMalformedResponseCategory, status.category) + } + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/message_actions/AddMessageActionTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/message_actions/AddMessageActionTestSuite.kt new file mode 100644 index 000000000..98b22bb74 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/message_actions/AddMessageActionTestSuite.kt @@ -0,0 +1,66 @@ +package com.pubnub.api.suite.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.message_actions.AddMessageAction +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNAddMessageActionResult +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class AddMessageActionTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_msga" + + override fun pnOperation() = PNOperationType.PNAddMessageAction + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): AddMessageAction { + return pubnub.addMessageAction().apply { + channel = "ch1" + messageAction = PNMessageAction( + type = "reaction", + value = "smiley", + messageTimetoken = 1000 + ) + } + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "data": { + "messageTimetoken": "123", + "type": "reaction", + "uuid": "someUuid", + "value": "smiley", + "actionTimetoken": "1000" + } + } + """.trimIndent() + + + override fun unsuccessfulResponseBodyList() = listOf( + """{"status":200,"data":null}""", + """{"status":200}""" + ) + + override fun verifyResultExpectations(result: PNAddMessageActionResult) { + assertEquals(123L, result.messageTimetoken) + assertEquals("reaction", result.type) + assertEquals("someUuid", result.uuid) + assertEquals("smiley", result.value) + assertEquals(1000L, result.actionTimetoken) + } + + override fun mappingBuilder() = + post(urlMatching("/v1/message-actions/mySubscribeKey/channel/ch1/message/1000.*")) + .withRequestBody( + equalToJson("""{"type":"reaction","value":"smiley"}""") + ) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsMultipleTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsMultipleTestSuite.kt new file mode 100644 index 000000000..82a658303 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsMultipleTestSuite.kt @@ -0,0 +1,77 @@ +package com.pubnub.api.suite.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.message_actions.GetMessageActions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNGetMessageActionsResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class GetMessageActionsMultipleTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_msga" + + override fun pnOperation() = PNOperationType.PNGetMessageActions + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): GetMessageActions { + return pubnub.getMessageActions().apply { + channel = "ch1" + limit = 5 + start = 991 + end = 969 + } + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "100", + "type": "reaction", + "uuid": "abc", + "value": "😀", + "actionTimetoken": "970" + }, + { + "messageTimetoken": "100", + "type": "reaction", + "uuid": "abc", + "value": "😬", + "actionTimetoken": "980" + }, + { + "messageTimetoken": "200", + "type": "action", + "uuid": "xyz", + "value": "😀", + "actionTimetoken": "990" + } + ] + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"status":200,"data":null}""", + """{"status":200}""" + ) + + override fun verifyResultExpectations(result: PNGetMessageActionsResult) { + assertEquals(3, result.actions.size) + assertEquals(2, result.actions.filter { it.uuid == "abc" }.size) + assertEquals(2, result.actions.filter { it.value == "\uD83D\uDE00" }.size) + assertEquals(2, result.actions.filter { it.messageTimetoken == 100L }.size) + } + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/ch1")) + .withQueryParam("start", equalTo("991")) + .withQueryParam("end", equalTo("969")) + .withQueryParam("limit", equalTo("5")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsTestSuite.kt new file mode 100644 index 000000000..e68c585d9 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/message_actions/GetMessageActionsTestSuite.kt @@ -0,0 +1,65 @@ +package com.pubnub.api.suite.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.message_actions.GetMessageActions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNGetMessageActionsResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class GetMessageActionsTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_msga" + + override fun pnOperation() = PNOperationType.PNGetMessageActions + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): GetMessageActions { + return pubnub.getMessageActions().apply { + channel = "ch1" + } + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "data": [ + { + "messageTimetoken": "100", + "type": "reaction", + "uuid": "abc", + "value": "smiley", + "actionTimetoken": "200" + } + ] + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"status":200,"data":null}""", + """{"status":200}""" + ) + + override fun verifyResultExpectations(result: PNGetMessageActionsResult) { + assertEquals(1, result.actions.size) + result.actions[0].apply { + assertEquals(100L, messageTimetoken) + assertEquals("reaction", type) + assertEquals("abc", uuid) + assertEquals("smiley", value) + assertEquals(200L, actionTimetoken) + + } + } + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/ch1")) + .withQueryParam("start", absent()) + .withQueryParam("end", absent()) + .withQueryParam("limit", absent()) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/message_actions/RemoveMessageActionsTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/message_actions/RemoveMessageActionsTestSuite.kt new file mode 100644 index 000000000..40d405f75 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/message_actions/RemoveMessageActionsTestSuite.kt @@ -0,0 +1,47 @@ +package com.pubnub.api.suite.message_actions + +import com.github.tomakehurst.wiremock.client.WireMock.delete +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.message_actions.RemoveMessageAction +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class RemoveMessageActionsTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_msga" + + override fun pnOperation() = PNOperationType.PNDeleteMessageAction + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveMessageAction { + return pubnub.removeMessageAction().apply { + channel = "ch1" + messageTimetoken = 100 + actionTimetoken = 200 + } + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "data": {} + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun verifyResultExpectations(result: PNRemoveMessageActionResult) { + + } + + override fun mappingBuilder() = + delete(urlPathEqualTo("/v1/message-actions/mySubscribeKey/channel/ch1/message/100/action/200")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/GetStateTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/GetStateTestSuite.kt new file mode 100644 index 000000000..7a92ba489 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/GetStateTestSuite.kt @@ -0,0 +1,76 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.pubnub.api.endpoints.presence.GetState +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNGetStateResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.OptionalScenario +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse + +class GetStateTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNGetState + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): GetState { + return pubnub.getPresenceState().apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: PNGetStateResult) { + assertEquals(1, result.stateByUUID.keys.size) + assertEquals(JsonObject().apply { addProperty("text", "hello") }, result.stateByUUID["ch1"]) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "payload": { + "text": "hello" + }, + "uuid": "myUUID", + "channel": "ch1", + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder = + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/myUUID")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("""{"payload":{}}""") } + additionalChecks = { pnStatus, result -> + assertFalse(pnStatus.error) + assertEquals(JsonObject(), result!!.stateByUUID["ch1"]) + } + }, + OptionalScenario().apply { + responseBuilder = { withBody("""{"payload":null}""") } + additionalChecks = { pnStatus, result -> + assertFalse(pnStatus.error) + assertEquals(JsonNull.INSTANCE, result!!.stateByUUID["ch1"]) + } + } + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/HeartbeatTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/HeartbeatTestSuite.kt new file mode 100644 index 000000000..5fe6760e5 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/HeartbeatTestSuite.kt @@ -0,0 +1,49 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.presence.Heartbeat +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertTrue + +class HeartbeatTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNHeartbeatOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): Heartbeat { + return Heartbeat( + pubnub = pubnub, + channels = listOf("ch1") + ) + } + + override fun verifyResultExpectations(result: Boolean) { + assertTrue(result) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder = + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/heartbeat")) + .withQueryParam("heartbeat", equalTo(pubnub.configuration.presenceTimeout.toString())) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun voidResponse() = true + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/HereNowTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/HereNowTestSuite.kt new file mode 100644 index 000000000..50a38a4c4 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/HereNowTestSuite.kt @@ -0,0 +1,60 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.presence.HereNow +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNHereNowResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class HereNowTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNHereNowOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): HereNow { + return pubnub.hereNow().apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: PNHereNowResult) { + assertEquals(1, result.totalChannels) + assertEquals(1, result.totalOccupancy) + assertEquals(1, result.channels.size) + assertEquals("user_1", result.channels["ch1"]!!.occupants[0].uuid) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "occupancy": 1, + "uuids": [ + "user_1" + ], + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"occupancy": 0, "uuids": null}""", + """{"payload": {"channels": null, "total_channels": 0, "total_occupancy": 0}}""", + """{"payload": {}}""", + """{"payload": null}""" + ) + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/presence/sub_key/mySubscribeKey/channel/ch1")) + .withQueryParam("state", absent()) + .withQueryParam("disable_uuids", absent()) + .withQueryParam("channel-group", absent()) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/LeaveTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/LeaveTestSuite.kt new file mode 100644 index 000000000..508b011aa --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/LeaveTestSuite.kt @@ -0,0 +1,48 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.presence.Leave +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertTrue + +class LeaveTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNUnsubscribeOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): Leave { + return Leave(pubnub).apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: Boolean) { + assertTrue(result) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder = + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/leave")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun voidResponse() = true + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/SetStateTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/SetStateTestSuite.kt new file mode 100644 index 000000000..a975aa51c --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/SetStateTestSuite.kt @@ -0,0 +1,73 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.google.gson.JsonObject +import com.pubnub.api.endpoints.presence.SetState +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.presence.PNSetStateResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.* + +class SetStateTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNSetStateOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): SetState { + return pubnub.setPresenceState().apply { + channels = listOf("ch1") + state = mapOf("text" to "hello") + } + } + + override fun verifyResultExpectations(result: PNSetStateResult) { + assertEquals(JsonObject().apply { addProperty("text", "hello") }, result.state) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "payload": { + "text": "hello" + }, + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"payload":null}""" + ) + + override fun mappingBuilder(): MappingBuilder = + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/channel/ch1/uuid/myUUID/data")) + .withQueryParam("state", equalTo("""{"text":"hello"}""")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + result = Result.SUCCESS + responseBuilder = { withBody("""{"payload":{}}""") } + additionalChecks = { pnStatus: PNStatus, result: PNSetStateResult? -> + assertFalse(pnStatus.error) + assertTrue(result!!.state.asJsonObject.keySet().isEmpty()) + } + }, + OptionalScenario().apply { + result = Result.FAIL + responseBuilder = { withBody("""{"payload":null}""") } + additionalChecks = { pnStatus: PNStatus, result: PNSetStateResult? -> + assertTrue(pnStatus.error) + assertNull(result) + } + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/presence/WhereNowTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/presence/WhereNowTestSuite.kt new file mode 100644 index 000000000..f75c35a8b --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/presence/WhereNowTestSuite.kt @@ -0,0 +1,56 @@ +package com.pubnub.api.suite.presence + +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.pubnub.api.endpoints.presence.WhereNow +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.presence.PNWhereNowResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals + +class WhereNowTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pres" + + override fun pnOperation() = PNOperationType.PNWhereNowOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): WhereNow { + return pubnub.whereNow().apply { + + } + } + + override fun verifyResultExpectations(result: PNWhereNowResult) { + assertEquals(1, result.channels.size) + assertEquals("ch1", result.channels[0]) + } + + override fun successfulResponseBody() = """ + { + "status": 200, + "message": "OK", + "payload": { + "channels": [ + "ch1" + ] + }, + "service": "Presence" + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = listOf( + """{"payload":{"channels":null}}""", + """{"payload": {}}""", + """{"payload": null}""" + ) + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/presence/sub-key/mySubscribeKey/uuid/myUUID")) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishGetTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishGetTestSuite.kt new file mode 100644 index 000000000..56c8e2a62 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishGetTestSuite.kt @@ -0,0 +1,64 @@ +package com.pubnub.api.suite.pubsub + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.google.gson.Gson +import com.pubnub.api.endpoints.pubsub.Publish +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.PUB +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +class PublishGetTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pub" + + override fun pnOperation() = PNOperationType.PNPublishOperation + + override fun requiredKeys() = SUB + PUB + AUTH + + override fun snippet(): Publish { + return pubnub.publish().apply { + channel = "ch1" + message = "ch2" + } + } + + override fun verifyResultExpectations(result: PNPublishResult) { + assertEquals(15883272000000000L, result.timetoken) + } + + override fun successfulResponseBody() = """[1,"Sent","15883272000000000"]""" + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get( + urlPathEqualTo( + "/publish/myPublishKey/mySubscribeKey/0/ch1/0/%s".format( + URLEncoder.encode(Gson().toJson("ch2"), StandardCharsets.UTF_8.name()) + ) + ) + )!! + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + + /*companion object { + @JvmStatic + fun predefined(): Stream? { + return Stream.of( + Arguments.of("ch1"), + Arguments.of(123), + Arguments.of(3.14) + ) + } + }*/ + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishPostTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishPostTestSuite.kt new file mode 100644 index 000000000..345973286 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/pubsub/PublishPostTestSuite.kt @@ -0,0 +1,69 @@ +package com.pubnub.api.suite.pubsub + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.google.gson.Gson +import com.pubnub.api.endpoints.pubsub.Publish +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.PUB +import com.pubnub.api.suite.SUB +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals + +class PublishPostTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_pub" + + override fun pnOperation() = PNOperationType.PNPublishOperation + + override fun requiredKeys() = SUB + PUB + AUTH + + override fun snippet(): Publish { + return pubnub.publish().apply { + channel = "ch1" + message = mapOf( + "name" to "john", + "age" to 30, + "private" to false + ) + meta = JSONObject().apply { + put("city", "sf") + }.toMap() + usePost = true + } + } + + override fun verifyResultExpectations(result: PNPublishResult) { + assertEquals(15883272000000000L, result.timetoken) + } + + override fun successfulResponseBody() = """[1,"Sent","15883272000000000"]""" + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return post( + urlPathEqualTo("/publish/myPublishKey/mySubscribeKey/0/ch1/0") + ) + .withQueryParam( + "meta", equalToJson("""{"city":"sf"}""") + ) + .withRequestBody( + equalToJson( + Gson().toJson( + mapOf( + "name" to "john", + "age" to 30, + "private" to false + ) + ) + ) + )!! + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/pubsub/SignalTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/pubsub/SignalTestSuite.kt new file mode 100644 index 000000000..a751791d7 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/pubsub/SignalTestSuite.kt @@ -0,0 +1,53 @@ +package com.pubnub.api.suite.pubsub + +import com.github.tomakehurst.wiremock.client.MappingBuilder +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.google.gson.Gson +import com.pubnub.api.endpoints.pubsub.Signal +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.PUB +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +class SignalTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_sig" + + override fun pnOperation() = PNOperationType.PNSignalOperation + + override fun requiredKeys() = SUB + PUB + AUTH + + override fun snippet(): Signal { + return pubnub.signal().apply { + channel = "ch1" + message = "ch2" + } + } + + override fun verifyResultExpectations(result: PNPublishResult) { + assertEquals(15883272000000000L, result.timetoken) + } + + override fun successfulResponseBody() = """[1,"Sent","15883272000000000"]""" + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder(): MappingBuilder { + return get( + urlPathEqualTo( + "/signal/myPublishKey/mySubscribeKey/0/ch1/0/%s".format( + URLEncoder.encode(Gson().toJson("ch2"), StandardCharsets.UTF_8.name()) + ) + ) + )!! + } + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/pubsub/SubscribeTestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/pubsub/SubscribeTestSuite.kt new file mode 100644 index 000000000..062496e52 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/pubsub/SubscribeTestSuite.kt @@ -0,0 +1,56 @@ +package com.pubnub.api.suite.pubsub + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.pubsub.Subscribe +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.models.server.SubscribeEnvelope +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class SubscribeTestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "" + + override fun pnOperation() = PNOperationType.PNSubscribeOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): Subscribe { + return Subscribe(pubnub).apply { + channels = listOf("ch1") + } + } + + override fun verifyResultExpectations(result: SubscribeEnvelope) { + assertEquals(100, result.metadata.timetoken) + assertEquals("1", result.metadata.region) + assertTrue(result.messages.isEmpty()) + } + + override fun successfulResponseBody() = """ + { + "t": { + "t": "100", + "r": 1 + }, + "m": [] + } + """.trimIndent() + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/subscribe/mySubscribeKey/ch1/0")) + .withQueryParam("tt", absent()) + .withQueryParam("tr", absent()) + .withQueryParam("filter-expr", absent()) + .withQueryParam("state", absent()) + .withQueryParam("channel-group", absent()) + .withQueryParam("heartbeat", equalTo("300")) + + override fun affectedChannelsAndGroups() = listOf("ch1") to emptyList() + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV1TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV1TestSuite.kt new file mode 100644 index 000000000..097084750 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV1TestSuite.kt @@ -0,0 +1,68 @@ +package com.pubnub.api.suite.push.add + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.PubNubError +import com.pubnub.api.endpoints.push.AddChannelsToPush +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.push.PNPushAddChannelResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +class AddChannelsToPushV1TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNAddPushNotificationsOnChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): AddChannelsToPush { + return pubnub.addPushNotificationsOnChannels().apply { + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2") + deviceId = "12345" + } + } + + override fun verifyResultExpectations(result: PNPushAddChannelResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Modified Channels"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) + .withQueryParam("type", equalTo("gcm")) + .withQueryParam("add", equalTo("ch1,ch2")) + .withQueryParam("environment", absent()) + .withQueryParam("topic", absent()) + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to emptyList() + + override fun voidResponse() = true + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + val body = "{\"error\":\"Use of the mobile push notifications API requires Push Notifications" + + " which is not enabled for this subscribe key. Login to your PubNub Dashboard Account" + + " and enable Push Notifications. " + + "Contact support@pubnub.com if you require further assistance.\"}" + result = Result.FAIL + responseBuilder = { withBody(body).withStatus(400) } + pnError = PubNubError.HTTP_ERROR + additionalChecks = { status: PNStatus, _: PNPushAddChannelResult? -> + assertTrue(voidResponse()) + assertEquals(body, status.exception!!.jso) + } + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV2TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV2TestSuite.kt new file mode 100644 index 000000000..fd8c60a90 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/add/AddChannelsToPushV2TestSuite.kt @@ -0,0 +1,49 @@ +package com.pubnub.api.suite.push.add + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.AddChannelsToPush +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushAddChannelResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class AddChannelsToPushV2TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNAddPushNotificationsOnChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): AddChannelsToPush { + return pubnub.addPushNotificationsOnChannels().apply { + pushType = PNPushType.APNS2 + channels = listOf("ch1", "ch2") + deviceId = "12345" + topic = "news" + } + } + + override fun verifyResultExpectations(result: PNPushAddChannelResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Modified Channels"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/12345")) + .withQueryParam("type", absent()) + .withQueryParam("add", equalTo("ch1,ch2")) + .withQueryParam("environment", equalTo("development")) + .withQueryParam("topic", equalTo("news")) + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV1TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV1TestSuite.kt new file mode 100644 index 000000000..1fa224c27 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV1TestSuite.kt @@ -0,0 +1,63 @@ +package com.pubnub.api.suite.push.list + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.ListPushProvisions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.push.PNPushListProvisionsResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse + +class ListPushProvisionsV1TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNPushNotificationEnabledChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): ListPushProvisions { + return pubnub.auditPushChannelProvisions().apply { + pushType = PNPushType.FCM + deviceId = "12345" + } + } + + override fun verifyResultExpectations(result: PNPushListProvisionsResult) { + assertEquals(2, result.channels.size) + assertEquals("ch1", result.channels[0]) + assertEquals("ch2", result.channels[1]) + } + + override fun successfulResponseBody(): String { + return """["ch1", "ch2"]""" + } + + override fun unsuccessfulResponseBodyList() = listOf( + """["ch1","ch2",{}]""" + ) + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) + .withQueryParam("type", equalTo("gcm")) + .withQueryParam("environment", absent()) + .withQueryParam("topic", absent()) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("[]") } + result = Result.SUCCESS + additionalChecks = { status: PNStatus, result: PNPushListProvisionsResult? -> + assertFalse(status.error) + assertEquals(0, result!!.channels.size) + } + } + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV2TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV2TestSuite.kt new file mode 100644 index 000000000..b37b99ee4 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/list/ListPushProvisionsV2TestSuite.kt @@ -0,0 +1,64 @@ +package com.pubnub.api.suite.push.list + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.ListPushProvisions +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.PNStatus +import com.pubnub.api.models.consumer.push.PNPushListProvisionsResult +import com.pubnub.api.suite.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse + +class ListPushProvisionsV2TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNPushNotificationEnabledChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): ListPushProvisions { + return pubnub.auditPushChannelProvisions().apply { + pushType = PNPushType.APNS2 + deviceId = "12345" + topic = "news" + } + } + + override fun verifyResultExpectations(result: PNPushListProvisionsResult) { + assertEquals(2, result.channels.size) + assertEquals("ch1", result.channels[0]) + assertEquals("ch2", result.channels[1]) + } + + override fun successfulResponseBody(): String { + return """["ch1", "ch2"]""" + } + + override fun unsuccessfulResponseBodyList() = listOf( + """["ch1","ch2",{}]""" + ) + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/12345")) + .withQueryParam("type", absent()) + .withQueryParam("environment", equalTo("development")) + .withQueryParam("topic", equalTo("news")) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun optionalScenarioList(): List> { + return listOf( + OptionalScenario().apply { + responseBuilder = { withBody("[]") } + result = Result.SUCCESS + additionalChecks = { status: PNStatus, result: PNPushListProvisionsResult? -> + assertFalse(status.error) + assertEquals(0, result!!.channels.size) + } + } + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV1TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV1TestSuite.kt new file mode 100644 index 000000000..0ae27719a --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV1TestSuite.kt @@ -0,0 +1,46 @@ +package com.pubnub.api.suite.push.remove + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.RemoveAllPushChannelsForDevice +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveAllChannelsResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class RemoveAllFromPushV1TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNRemoveAllPushNotificationsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveAllPushChannelsForDevice { + return pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + pushType = PNPushType.FCM + deviceId = "12345" + } + } + + override fun verifyResultExpectations(result: PNPushRemoveAllChannelsResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Removed Device"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345/remove")) + .withQueryParam("type", equalTo("gcm")) + .withQueryParam("environment", absent()) + .withQueryParam("topic", absent()) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV2TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV2TestSuite.kt new file mode 100644 index 000000000..6f2d0a750 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveAllFromPushV2TestSuite.kt @@ -0,0 +1,48 @@ +package com.pubnub.api.suite.push.remove + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.RemoveAllPushChannelsForDevice +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveAllChannelsResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class RemoveAllFromPushV2TestSuite : + EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNRemoveAllPushNotificationsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveAllPushChannelsForDevice { + return pubnub.removeAllPushNotificationsFromDeviceWithPushToken().apply { + pushType = PNPushType.APNS2 + deviceId = "12345" + topic = "news" + } + } + + override fun verifyResultExpectations(result: PNPushRemoveAllChannelsResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Removed Device"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/12345/remove")) + .withQueryParam("type", absent()) + .withQueryParam("environment", equalTo("development")) + .withQueryParam("topic", equalTo("news")) + + override fun affectedChannelsAndGroups() = emptyList() to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt new file mode 100644 index 000000000..c7a702e06 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV1TestSuite.kt @@ -0,0 +1,48 @@ +package com.pubnub.api.suite.push.remove + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.RemoveChannelsFromPush +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class RemoveChannelsFromPushV1TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNRemovePushNotificationsFromChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveChannelsFromPush { + return pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.FCM + channels = listOf("ch1", "ch2") + deviceId = "12345" + } + } + + override fun verifyResultExpectations(result: PNPushRemoveChannelResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Modified Channels"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v1/push/sub-key/mySubscribeKey/devices/12345")) + .withQueryParam("type", equalTo("gcm")) + .withQueryParam("remove", equalTo("ch1,ch2")) + .withQueryParam("environment", absent()) + .withQueryParam("topic", absent()) + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV2TestSuite.kt b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV2TestSuite.kt new file mode 100644 index 000000000..2ff411155 --- /dev/null +++ b/src/test/kotlin/com/pubnub/api/suite/push/remove/RemoveChannelsFromPushV2TestSuite.kt @@ -0,0 +1,49 @@ +package com.pubnub.api.suite.push.remove + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.pubnub.api.endpoints.push.RemoveChannelsFromPush +import com.pubnub.api.enums.PNOperationType +import com.pubnub.api.enums.PNPushType +import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult +import com.pubnub.api.suite.AUTH +import com.pubnub.api.suite.EndpointTestSuite +import com.pubnub.api.suite.SUB + +class RemoveChannelsFromPushV2TestSuite : EndpointTestSuite() { + + override fun telemetryParamName() = "l_push" + + override fun pnOperation() = PNOperationType.PNRemovePushNotificationsFromChannelsOperation + + override fun requiredKeys() = SUB + AUTH + + override fun snippet(): RemoveChannelsFromPush { + return pubnub.removePushNotificationsFromChannels().apply { + pushType = PNPushType.APNS2 + channels = listOf("ch1", "ch2") + deviceId = "12345" + topic = "news" + } + } + + override fun verifyResultExpectations(result: PNPushRemoveChannelResult) { + + } + + override fun successfulResponseBody(): String { + return """[1, "Modified Channels"]""" + } + + override fun unsuccessfulResponseBodyList() = emptyList() + + override fun mappingBuilder() = + get(urlPathEqualTo("/v2/push/sub-key/mySubscribeKey/devices-apns2/12345")) + .withQueryParam("type", absent()) + .withQueryParam("remove", equalTo("ch1,ch2")) + .withQueryParam("environment", equalTo("development")) + .withQueryParam("topic", equalTo("news")) + + override fun affectedChannelsAndGroups() = listOf("ch1", "ch2") to emptyList() + + override fun voidResponse() = true +} \ No newline at end of file diff --git a/src/test/resources/hmacsha256.py b/src/test/resources/hmacsha256.py new file mode 100644 index 000000000..ab2092325 --- /dev/null +++ b/src/test/resources/hmacsha256.py @@ -0,0 +1,34 @@ +#!/usr/bin/python + +import hashlib +import hmac +import base64 +import sys +import subprocess + +secret = sys.argv[1] +data = sys.argv[2].replace("###", "\n") +is_v2_signature = sys.argv[3] + +message = "{0}".format(data) + +hash = hmac.new(secret, message, hashlib.sha256) + +# to lowercase hexits +hash.hexdigest() + +# to base64 +result = base64.b64encode(hash.digest()) + +result = result.replace('+','-') +result = result.replace('/','_') +result = result.replace('/n','') + +if is_v2_signature == 'true': + while result.endswith('='): + result = result[:-1] + signature = 'v2.'+result +else: + signature = result + +print(signature) \ No newline at end of file diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..454391d99 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.execution.parallel.enabled = false \ No newline at end of file diff --git a/src/test/resources/special_chars.json b/src/test/resources/special_chars.json new file mode 100644 index 000000000..286418ca2 --- /dev/null +++ b/src/test/resources/special_chars.json @@ -0,0 +1,327 @@ +[ + { + "name": "space", + "regular": " ", + "encoded": "%20" + }, + { + "name": "double_quote", + "regular": "\"", + "encoded": "%22" + }, + { + "name": "single_quote", + "regular": "'", + "encoded": "%27" + }, + { + "name": "exclamation", + "regular": "!", + "encoded": "%21" + }, + { + "name": "right_double_quotation_mark", + "regular": "”", + "encoded": "%E2%80%9D" + }, + { + "name": "hash", + "regular": "#", + "encoded": "%23" + }, + { + "name": "dollar_sign", + "regular": "$", + "encoded": "%24" + }, + { + "name": "percent", + "regular": "%", + "encoded": "%25" + }, + { + "name": "ampersand", + "regular": "&", + "encoded": "%26" + }, + { + "name": "right_single_quotation_mark", + "regular": "’", + "encoded": "%E2%80%99" + }, + { + "name": "left_parenthesis", + "regular": "(", + "encoded": "%28" + }, + { + "name": "right_parenthesis", + "regular": ")", + "encoded": "%29" + }, + { + "name": "asterisk", + "regular": "*", + "encoded": "*" + }, + { + "name": "plus", + "regular": "+", + "encoded": "%2B" + }, + { + "name": "comma", + "regular": ",", + "encoded": "%2C" + }, + { + "name": "minus", + "regular": "-", + "encoded": "-" + }, + { + "name": "full_stop", + "regular": ".", + "encoded": "." + }, + { + "name": "slash", + "regular": "/", + "encoded": "%2F" + }, + { + "name": "colon", + "regular": ":", + "encoded": "%3A" + }, + { + "name": "semicolon", + "regular": ";", + "encoded": "%3B" + }, + { + "name": "less_than", + "regular": "<", + "encoded": "%3C" + }, + { + "name": "equal_sign", + "regular": "=", + "encoded": "%3D" + }, + { + "name": "greater_than", + "regular": ">", + "encoded": "%3E" + }, + { + "name": "question_mark", + "regular": "?", + "encoded": "%3F" + }, + { + "name": "at_sign", + "regular": "@", + "encoded": "%40" + }, + { + "name": "left_bracket", + "regular": "[", + "encoded": "%5B" + }, + { + "name": "backslash", + "regular": "\\", + "encoded": "%5C" + }, + { + "name": "right_bracket", + "regular": "]", + "encoded": "%5D" + }, + { + "name": "caret", + "regular": "^", + "encoded": "%5E" + }, + { + "name": "underscore", + "regular": "_", + "encoded": "_" + }, + { + "name": "backtick", + "regular": "`", + "encoded": "%60" + }, + { + "name": "left_brace", + "regular": "{", + "encoded": "%7B" + }, + { + "name": "vertical_bar", + "regular": "|", + "encoded": "%7C" + }, + { + "name": "right_brace", + "regular": "}", + "encoded": "%7D" + }, + { + "name": "tilde", + "regular": "~", + "encoded": "%7E" + }, + { + "name": "inverted_exclamation_mark", + "regular": "¡", + "encoded": "%C2%A1" + }, + { + "name": "cent_sign", + "regular": "¢", + "encoded": "%C2%A2" + }, + { + "name": "pound_sign", + "regular": "£", + "encoded": "%C2%A3" + }, + { + "name": "currency_sign", + "regular": "¤", + "encoded": "%C2%A4" + }, + { + "name": "yen_sign", + "regular": "¥", + "encoded": "%C2%A5" + }, + { + "name": "broken_bar", + "regular": "¦", + "encoded": "%C2%A6" + }, + { + "name": "section_sign", + "regular": "§", + "encoded": "%C2%A7" + }, + { + "name": "diaeresis", + "regular": "¨", + "encoded": "%C2%A8" + }, + { + "name": "copyright_sign", + "regular": "©", + "encoded": "%C2%A9" + }, + { + "name": "feminine_ordinal_indicator", + "regular": "ª", + "encoded": "%C2%AA" + }, + { + "name": "left-pointing_double_angle_quotation_mark", + "regular": "«", + "encoded": "%C2%AB" + }, + { + "name": "not_sign", + "regular": "¬", + "encoded": "%C2%AC" + }, + { + "name": "registered_sign", + "regular": "®", + "encoded": "%C2%AE" + }, + { + "name": "macron", + "regular": "¯", + "encoded": "%C2%AF" + }, + { + "name": "degree_sign", + "regular": "°", + "encoded": "%C2%B0" + }, + { + "name": "plus-minus_sign", + "regular": "±", + "encoded": "%C2%B1" + }, + { + "name": "superscript_two", + "regular": "²", + "encoded": "%C2%B2" + }, + { + "name": "superscript_three", + "regular": "³", + "encoded": "%C2%B3" + }, + { + "name": "acute_accent", + "regular": "´", + "encoded": "%C2%B4" + }, + { + "name": "micro_sign", + "regular": "µ", + "encoded": "%C2%B5" + }, + { + "name": "pilcrow_sign", + "regular": "¶", + "encoded": "%C2%B6" + }, + { + "name": "middle_dot", + "regular": "·", + "encoded": "%C2%B7" + }, + { + "name": "cedilla", + "regular": "¸", + "encoded": "%C2%B8" + }, + { + "name": "superscript_one", + "regular": "¹", + "encoded": "%C2%B9" + }, + { + "name": "masculine_ordinal_indicator", + "regular": "º", + "encoded": "%C2%BA" + }, + { + "name": "right-pointing_double_angle_quotation_mark", + "regular": "»", + "encoded": "%C2%BB" + }, + { + "name": "vulgar_fraction_one_quarter", + "regular": "¼", + "encoded": "%C2%BC" + }, + { + "name": "vulgar_fraction_one_half", + "regular": "½", + "encoded": "%C2%BD" + }, + { + "name": "vulgar_fraction_three_quarters", + "regular": "¾", + "encoded": "%C2%BE" + }, + { + "name": "inverted_question_mark", + "regular": "¿", + "encoded": "%C2%BF" + } +] \ No newline at end of file