diff --git a/build.gradle b/build.gradle index e87b7c62..56f1855d 100644 --- a/build.gradle +++ b/build.gradle @@ -41,9 +41,17 @@ sourceSets { test.java.srcDirs 'src/test' } +test { + useJUnitPlatform() +} + dependencies { + + // JUnit Tests testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.8.0' + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0") + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' //Jitpack imports implementation 'com.github.nwaldispuehl:java-lame:v3.98.4' diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/LoginDialog.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/LoginDialog.java index ed081631..f30ef455 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/LoginDialog.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/LoginDialog.java @@ -49,6 +49,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.ConnectException; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; /** * Login dialog for radioreference.com with support for testing connection to the service. @@ -66,6 +69,7 @@ public class LoginDialog extends Dialog private Button mTestConnectionButton; private IconNode mTestPassIcon; private IconNode mTestFailIcon; + private IconNode mTextExpiredIcon; /** * Constructs an instance. @@ -166,6 +170,10 @@ public class LoginDialog extends Dialog getTestFailIcon().setVisible(false); GridPane.setConstraints(getTestFailIcon(), 2, 4); mGridPane.getChildren().add(getTestFailIcon()); + + getTestExpiredIcon().setVisible(false); + GridPane.setConstraints(getTestExpiredIcon(), 2, 4); + mGridPane.getChildren().add(getTestExpiredIcon()); } return mGridPane; @@ -275,6 +283,7 @@ public class LoginDialog extends Dialog getTestConnectionButton().setDisable(true); getTestPassIcon().setVisible(false); getTestFailIcon().setVisible(false); + getTestExpiredIcon().setVisible(false); String userName = getUserNameText().getText(); String password = getPasswordField().isVisible() ? getPasswordField().getText() : getPasswordText().getText(); @@ -307,13 +316,56 @@ public class LoginDialog extends Dialog public void run() { boolean success = false; + RadioReference.LoginStatus loginStatus; try { - success = RadioReference.testConnection(userName, password); + loginStatus = RadioReference.testConnectionWithExp(userName, password); + + if(loginStatus == RadioReference.LoginStatus.VALID_PREMIUM) + { + getTestPassIcon().setVisible(true); + getTestFailIcon().setVisible(false); + getTestExpiredIcon().setVisible(false); + } + else if (loginStatus == RadioReference.LoginStatus.EXPIRED_PREMIUM) + { + getTestPassIcon().setVisible(false); + getTestFailIcon().setVisible(false); + getTestExpiredIcon().setVisible(true); + + Alert alert = new Alert(Alert.AlertType.WARNING, "You do not have a valid Radio Reference Premium Subscription (it may have expired).", ButtonType.OK); + alert.setHeaderText("Expired Premium Account"); + alert.setTitle("Radio Reference Account issue"); + alert.initOwner(((Node)getTestConnectionButton()).getScene().getWindow()); + alert.showAndWait(); + } + else if (loginStatus == RadioReference.LoginStatus.INVALID_LOGIN) + { + getTestPassIcon().setVisible(false); + getTestFailIcon().setVisible(true); + getTestExpiredIcon().setVisible(false); + + Alert alert = new Alert(Alert.AlertType.ERROR, "Please verify username and password", ButtonType.OK); + alert.setHeaderText("Login Failed"); + alert.setTitle("Test Failed"); + alert.initOwner(((Node)getTestConnectionButton()).getScene().getWindow()); + alert.showAndWait(); + mLog.error("Login failed. Invalid username or password. Can't login to radioreference.com"); + } + else + { + // Only way to get here is via an exception + } } catch(RadioReferenceException rre) { + + // Set an error state + getTestPassIcon().setVisible(false); + getTestFailIcon().setVisible(true); + getTestExpiredIcon().setVisible(false); + if(rre.getCause() instanceof ConnectException) { Alert alert = new Alert(Alert.AlertType.ERROR, "Please check network or radio reference availability", ButtonType.OK); @@ -327,24 +379,12 @@ public class LoginDialog extends Dialog { Fault fault = rre.getFault(); - if(fault.getFaultCode() != null && fault.getFaultCode().contentEquals("AUTH")) - { - Alert alert = new Alert(Alert.AlertType.ERROR, "Please verify username and password", ButtonType.OK); - alert.setHeaderText("Login Failed"); - alert.setTitle("Test Failed"); - alert.initOwner(((Node)getTestConnectionButton()).getScene().getWindow()); - alert.showAndWait(); - mLog.error("Login failed. Invalid username or password. Can't login to radioreference.com"); - } - else - { - Alert alert = new Alert(Alert.AlertType.ERROR, fault.getFaultString(), ButtonType.OK); - alert.setHeaderText(fault.getFaultCode()); - alert.setTitle("Test Failed"); - alert.initOwner(((Node)getTestConnectionButton()).getScene().getWindow()); - alert.showAndWait(); - mLog.error("Test failed. Fault [" + fault.toString() + "] Can't login to radioreference.com"); - } + Alert alert = new Alert(Alert.AlertType.ERROR, fault.getFaultString(), ButtonType.OK); + alert.setHeaderText(fault.getFaultCode()); + alert.setTitle("Test Failed"); + alert.initOwner(((Node)getTestConnectionButton()).getScene().getWindow()); + alert.showAndWait(); + mLog.error("Test failed. Fault [" + fault.toString() + "] Can't login to radioreference.com"); } else { @@ -357,8 +397,7 @@ public class LoginDialog extends Dialog } } - getTestPassIcon().setVisible(success); - getTestFailIcon().setVisible(!success); + getTestConnectionButton().setDisable(false); } }); @@ -392,4 +431,16 @@ public class LoginDialog extends Dialog return mTestFailIcon; } + + private IconNode getTestExpiredIcon() + { + if(mTextExpiredIcon == null) + { + mTextExpiredIcon = new IconNode(FontAwesome.EXCLAMATION_TRIANGLE); + mTextExpiredIcon.setIconSize(32); + mTextExpiredIcon.setFill(Color.ORANGERED); + } + + return mTextExpiredIcon; + } } diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceEditor.java index 2e990dfb..8667fb88 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceEditor.java @@ -35,6 +35,7 @@ import io.github.dsheirer.rrapi.type.StateInfo; import io.github.dsheirer.service.radioreference.RadioReference; import io.github.dsheirer.util.ThreadPool; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -43,12 +44,7 @@ import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextField; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.util.StringConverter; import jiconfont.icons.font_awesome.FontAwesome; @@ -67,10 +63,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer mCountryComboBox; @@ -99,13 +97,13 @@ public class RadioReferenceEditor extends BorderPane implements Consumer { + mRadioReference.premiumAccountProperty().addListener((observable, oldValue, newValue) -> { if(newValue) { refreshCountries(); @@ -173,6 +171,7 @@ public class RadioReferenceEditor extends BorderPane implements Consumer typeMap = mRadioReference.getService().getTypesMap(); - Map flavorMap = mRadioReference.getService().getFlavorsMap(); - Map voiceMap = mRadioReference.getService().getVoicesMap(); - Map tagMap = mRadioReference.getService().getTagsMap(); - mRadioReferenceDecoder = new RadioReferenceDecoder(mUserPreferences, typeMap, flavorMap, - voiceMap, tagMap); + initRadioReferenceDecoder(); } //Query and load the system view editor first @@ -320,6 +328,20 @@ public class SystemEditor extends VBox } } + /** + * Initializes the Radio Reference Decoder + * @throws RadioReferenceException + */ + private void initRadioReferenceDecoder() throws RadioReferenceException + { + Map typeMap = mRadioReference.getService().getTypesMap(); + Map flavorMap = mRadioReference.getService().getFlavorsMap(); + Map voiceMap = mRadioReference.getService().getVoicesMap(); + Map tagMap = mRadioReference.getService().getTagsMap(); + mRadioReferenceDecoder = new RadioReferenceDecoder(mUserPreferences, typeMap, flavorMap, + voiceMap, tagMap); + } + public class SystemListCell extends ListCell { private HBox mHBox; diff --git a/src/main/java/io/github/dsheirer/service/radioreference/RadioReference.java b/src/main/java/io/github/dsheirer/service/radioreference/RadioReference.java index 22d8e0bc..e259c2ca 100644 --- a/src/main/java/io/github/dsheirer/service/radioreference/RadioReference.java +++ b/src/main/java/io/github/dsheirer/service/radioreference/RadioReference.java @@ -22,18 +22,22 @@ package io.github.dsheirer.service.radioreference; +import com.google.common.annotations.VisibleForTesting; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.rrapi.RadioReferenceException; import io.github.dsheirer.rrapi.RadioReferenceService; +import io.github.dsheirer.rrapi.response.Fault; import io.github.dsheirer.rrapi.type.AuthorizationInformation; import io.github.dsheirer.rrapi.type.UserInfo; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; import java.util.Scanner; /** @@ -51,7 +55,19 @@ public class RadioReference private StringProperty mPassword = new SimpleStringProperty(); private StringProperty mAccountExpiresProperty = new SimpleStringProperty(); private BooleanProperty mAvailable = new SimpleBooleanProperty(); + private BooleanProperty mPremiumAccount = new SimpleBooleanProperty(); + private LoginStatus mLoginStatus = LoginStatus.UNKNOWN; + /** + * Enum to determine "validity" of a RR Login + */ + public enum LoginStatus + { + UNKNOWN, + INVALID_LOGIN, + EXPIRED_PREMIUM, + VALID_PREMIUM + } /** * Constructs an instance of the radio reference service @@ -70,6 +86,22 @@ public class RadioReference return mAvailable; } + /** + * Externally monitored and controlled status indicator for a Premium RR Account + */ + public BooleanProperty premiumAccountProperty() + { + return mPremiumAccount; + } + + /** + * Current login state / validity + */ + public LoginStatus getLoginStatus() + { + return mLoginStatus; + } + /** * User name for radio reference account */ @@ -140,7 +172,94 @@ public class RadioReference } /** - * Login with credentials + * Tests the connection to radio reference service using the provided credentials. + * @param userName for radio reference account + * @param password for radio reference account + * @return The state and "validity" of the account + * @throws RadioReferenceException if there are any issues with the connection + */ + public static LoginStatus testConnectionWithExp(String userName, String password) throws RadioReferenceException + { + + AuthorizationInformation credentials = new AuthorizationInformation(SDRTRUNK_APP_KEY, userName, password); + + try + { + RadioReferenceService service = new RadioReferenceService(credentials); + UserInfo ui = service.getUserInfo(); + + return CheckExpDate(ui.getExpirationDate()); + } + catch (RadioReferenceException rre) + { + if (rre.hasFault()) + { + Fault fault = rre.getFault(); + + if (fault.getFaultCode() != null && fault.getFaultCode().contentEquals("AUTH")) + { + return LoginStatus.INVALID_LOGIN; + } + else + { + throw rre; + } + } + else + { + throw rre; + } + } + } + + /** + * Determines the account status (Premium) based on the given expiration date + * @param RRExpirationString The Premium Expiration string returned from RR + * @return Account Login Status + */ + @VisibleForTesting + protected static LoginStatus CheckExpDate(String RRExpirationString) + { + try + { + SimpleDateFormat formatter = new SimpleDateFormat("MM-dd-yyyy", Locale.ENGLISH); + Date RRExpDate = formatter.parse(RRExpirationString); + + // We want to err on the side of the user getting an error with an invalid subscription, + // rather than the user being prevented from using their valid, but shortly expiring, subscription. + + // Get the current Date and subtract two days (due to no >=, only > ), + // as I am not sure how timezones work for RR and the end user. + + Calendar c = Calendar.getInstance(); + c.setTime(new Date()); + c.add(Calendar.DAY_OF_MONTH, -2); + + if(RRExpDate.after(c.getTime())) + { + return LoginStatus.VALID_PREMIUM; + } + else + { + return LoginStatus.EXPIRED_PREMIUM; + } + } + catch (ParseException e) + { + // This doesn't have to be a date; it can also be a string + // If we get anything we can't parse, assume it's a string and valid + // Accounts that have never had a valid subscription return a year of 1969 + + // See https://wiki.radioreference.com/index.php/RadioReference.com_Web_Service3.1#Versions : + // Version 5 - Issued a fix for the getUserData method which returned a subscription expiration + // date in 1969 for admins and feed providers. Now returns "Never - Feed Provider" and "Never - Admin" + // in string format. + return LoginStatus.VALID_PREMIUM; + } + } + + /** + * Login with credentials, and update properties based on the login status */ private void login() { @@ -148,12 +267,31 @@ public class RadioReference { UserInfo userInfo = getService().getUserInfo(); accountExpiresProperty().setValue(userInfo.getExpirationDate()); - availableProperty().set(true); + + mLoginStatus = CheckExpDate(userInfo.getExpirationDate()); + if(mLoginStatus == LoginStatus.VALID_PREMIUM) + { + availableProperty().set(true); + premiumAccountProperty().set(true); + } + else if (mLoginStatus == LoginStatus.EXPIRED_PREMIUM) + { + availableProperty().set(true); + premiumAccountProperty().set(false); + } + else + { + availableProperty().set(false); + premiumAccountProperty().set(false); + } } catch(RadioReferenceException rre) { accountExpiresProperty().setValue(null); + mLoginStatus = LoginStatus.UNKNOWN; availableProperty().set(false); + premiumAccountProperty().set(false); + } } diff --git a/src/test/java/io/github/dsheirer/service/radioreference/RadioReferenceTest.java b/src/test/java/io/github/dsheirer/service/radioreference/RadioReferenceTest.java new file mode 100644 index 00000000..fae4f1d4 --- /dev/null +++ b/src/test/java/io/github/dsheirer/service/radioreference/RadioReferenceTest.java @@ -0,0 +1,54 @@ +package io.github.dsheirer.service.radioreference; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import static io.github.dsheirer.service.radioreference.RadioReference.CheckExpDate; +import static org.junit.Assert.*; + +public class RadioReferenceTest +{ + + /** + * Tests the handling of Radio Reference's Premium Expiration date to Account Statuses + * Necessary because DateTime math gets easy to do incorrectly, and we don't want to + * lock a user out of access due to an off-by-one Date issue. + */ + @Test + void checkExpDate() + { + SimpleDateFormat RRDateFormatter = new SimpleDateFormat("MM-dd-yyyy"); + Date now = new Date(); + + Calendar c = Calendar.getInstance(); + c.setTime(now); + + // Never had premium (expired) + Assertions.assertEquals(RadioReference.LoginStatus.EXPIRED_PREMIUM, CheckExpDate("12-31-1969")); + + // Expiring today (valid) + Assertions.assertEquals(RadioReference.LoginStatus.VALID_PREMIUM, CheckExpDate(RRDateFormatter.format(c.getTime()))); + + // Expiring next week (valid) + c.add(Calendar.DAY_OF_MONTH, 7); + Assertions.assertEquals(RadioReference.LoginStatus.VALID_PREMIUM, CheckExpDate(RRDateFormatter.format(c.getTime()))); + + // Expired yesterday (valid) + c.setTime(now); + c.add(Calendar.DAY_OF_MONTH, -1); + Assertions.assertEquals(RadioReference.LoginStatus.VALID_PREMIUM, CheckExpDate(RRDateFormatter.format(c.getTime()))); + + // Expired last week (expired) + c.setTime(now); + c.add(Calendar.DAY_OF_MONTH, -7); + Assertions.assertEquals(RadioReference.LoginStatus.EXPIRED_PREMIUM, CheckExpDate(RRDateFormatter.format(c.getTime()))); + + // Broadcastify user? Admin? Any string that's not a Date is considered valid (valid) + Assertions.assertEquals(RadioReference.LoginStatus.VALID_PREMIUM, CheckExpDate("Not a date")); + + } +} \ No newline at end of file