From b1859aca3f49e7d7e33b81673e71bd8c9c3c64fc Mon Sep 17 00:00:00 2001 From: DanVanAtta Date: Fri, 21 Jun 2024 22:57:32 -0700 Subject: [PATCH] Bulk import & start organizing java files from 'triplea/triplea-game' --- .docker-compose/connect-to-db.sh | 12 + .docker-compose/database/01-init.sql | 5 + .docker-compose/nginx/default.conf | 57 +++ Dockerfile | 8 + build.gradle | 55 ++- docker-compose.yml | 69 +++ http-clients/github-client/build.gradle | 13 + .../client/github/BranchInfoResponse.java | 42 ++ .../client/github/CreateIssueRequest.java | 31 ++ .../client/github/CreateIssueResponse.java | 15 + .../http/client/github/GithubApiClient.java | 142 ++++++ .../client/github/GithubApiFeignClient.java | 38 ++ .../client/github/LatestReleaseResponse.java | 12 + .../http/client/github/MapRepoListing.java | 25 + .../client/github/GithubApiClientTest.java | 124 +++++ .../src/test/resources/logback-test.xml | 11 + .../branch_listing_response.json | 97 ++++ .../latest_release_response.json | 346 +++++++++++++ .../repo_listing_response_page1.json | 200 ++++++++ .../repo_listing_response_page2.json | 101 ++++ server/README-WIP.md | 12 + server/README.md | 26 + server/database-test-support/build.gradle | 10 + .../database/DatabaseTestSupport.java | 18 + .../database/DbRiderTestExtension.java | 101 ++++ server/database/.dockerignore | 2 + server/database/README.md | 24 + server/database/build.gradle | 40 ++ server/database/connect_to_docker_db | 9 + server/database/load_sample_data | 11 + server/database/reset_docker_db | 28 ++ server/database/sql/init/01-create-users.sql | 2 + .../database/sql/init/02-create-databases.sql | 2 + .../sql/sample_data/lobby_db_sample_data.sql | 115 +++++ .../error_report/V1.00.00__create_tables.sql | 43 ++ .../db/migration/lobby_db/V2.00.00__lobby.sql | 224 +++++++++ .../lobby_db/V2.00.01__add_chat_history.sql | 23 + .../V2.00.02__game_and_player_history.sql | 39 ++ .../V2.00.03__change_game_history_index.sql | 11 + ...00.04__add_lobby_user_fk_to_access_log.sql | 3 + .../V2.00.05__drop_legacy_password.sql | 2 + ...6__error_report_issue_and_title_column.sql | 30 ++ .../V2.01.00__fix_username_ban_casing.sql | 11 + .../lobby_db/V2.01.01__maps_index_tables.sql | 33 ++ .../migration/lobby_db/V2.01.02__map_tags.sql | 34 ++ .../lobby_db/V2.02.00__lobby_message.sql | 9 + server/database/start_docker_db | 51 ++ server/dropwizard-server/build.gradle | 75 +++ .../spitfire/server/ResponseStatus.java | 31 ++ .../server/SpitfireServerApplication.java | 167 +++++++ .../spitfire/server/SpitfireServerConfig.java | 77 +++ .../authentication/ApiKeyAuthenticator.java | 53 ++ .../authentication/AuthenticatedUser.java | 46 ++ .../authorization/BannedPlayerFilter.java | 75 +++ .../access/authorization/RoleAuthorizer.java | 47 ++ .../version/LatestVersionController.java | 36 ++ .../lobby/GameHostingController.java | 50 ++ .../lobby/GameListingController.java | 53 ++ .../lobby/LobbyWatcherController.java | 189 ++++++++ .../lobby/PlayersInGameController.java | 32 ++ .../lobby/moderation/AccessLogController.java | 37 ++ .../lobby/moderation/BadWordsController.java | 67 +++ .../moderation/DisconnectUserController.java | 44 ++ .../moderation/GameChatHistoryController.java | 32 ++ .../ModeratorAuditHistoryController.java | 49 ++ .../moderation/ModeratorsController.java | 80 ++++ .../lobby/moderation/MuteUserController.java | 39 ++ .../moderation/RemoteActionsController.java | 58 +++ .../lobby/moderation/UserBanController.java | 89 ++++ .../moderation/UsernameBanController.java | 61 +++ .../user/account/CreateAccountController.java | 47 ++ .../account/ForgotPasswordController.java | 55 +++ .../user/account/LoginController.java | 49 ++ .../user/account/PlayerInfoController.java | 41 ++ .../user/account/UpdateAccountController.java | 66 +++ .../src/main/resources/configuration.yml | 55 +++ .../common/IpAddressExtractorTest.java | 47 ++ .../server/ControllerIntegrationTest.java | 114 +++++ .../server/DropwizardServerExtension.java | 77 +++ .../server/SpitfireDatabaseTestSupport.java | 18 + .../server/SpitfireServerTestExtension.java | 19 + .../org/triplea/spitfire/server/TestData.java | 25 + .../ApiKeyAuthenticatorTest.java | 78 +++ .../authentication/AuthenticatedUserTest.java | 124 +++++ .../BannedPlayerFilterIntegrationTest.java | 56 +++ .../authorization/BannedPlayerFilterTest.java | 132 +++++ .../authorization/RoleAuthorizerTest.java | 90 ++++ .../AccessLogControllerIntegrationTest.java | 100 ++++ .../BadWordsControllerIntegrationTest.java | 56 +++ ...reateAccountControllerIntegrationTest.java | 56 +++ ...sconnectUserControllerIntegrationTest.java | 34 ++ ...rgotPasswordControllerIntegrationTest.java | 27 ++ ...eChatHistoryControllerIntegrationTest.java | 19 + .../GameHostingControllerTest.java | 24 + .../GameListingControllerTest.java | 24 + .../GameListingWebsocketIntegrationTest.java | 137 ++++++ .../controllers/LobbyChatIntegrationTest.java | 257 ++++++++++ .../LobbyWatcherControllerTest.java | 88 ++++ .../LobbyWebsocketClientIntegrationTest.java | 93 ++++ .../LoginControllerIntegrationTest.java | 53 ++ ...AuditHistoryControllerIntegrationTest.java | 45 ++ .../ModeratorsControllerIntegrationTest.java | 60 +++ .../MuteUserControllerIntegrationTest.java | 20 + .../PlayerInfoControllerIntegrationTest.java | 59 +++ ...emoteActionsControllerIntegrationTest.java | 66 +++ .../UserAccountControllerIntegrationTest.java | 52 ++ .../UserBanControllerIntegrationTest.java | 88 ++++ .../UsernameBanControllerIntegrationTest.java | 93 ++++ .../test/resources/datasets/access_log.yml | 6 + .../src/test/resources/datasets/bad_word.yml | 7 + .../test/resources/datasets/banned_user.yml | 15 + .../resources/datasets/banned_username.yml | 7 + .../datasets/game_hosting_api_key.yml | 6 + .../test/resources/datasets/lobby_api_key.yml | 33 ++ .../test/resources/datasets/lobby_user.yml | 22 + .../src/test/resources/datasets/map_index.yml | 19 + .../test/resources/datasets/map_tag_value.yml | 35 ++ .../datasets/moderator_action_history.yml | 6 + .../datasets/temp_password_request.yml | 6 + .../src/test/resources/datasets/user_role.yml | 11 + .../src/test/resources/db-cleanup.sql | 16 + .../src/test/resources/dbunit.yml | 5 + server/latest-version-module/README.md | 10 + server/latest-version-module/build.gradle | 12 + .../latest/version/LatestVersionModule.java | 50 ++ .../version/LatestVersionModuleConfig.java | 7 + server/lobby-module/README.md | 189 ++++++++ server/lobby-module/build.gradle | 33 ++ .../org/triplea/db/LobbyModuleRowMappers.java | 46 ++ .../db/dao/access/log/AccessLogDao.java | 44 ++ .../db/dao/access/log/AccessLogRecord.java | 30 ++ .../triplea/db/dao/api/key/ApiKeyHasher.java | 14 + .../db/dao/api/key/GameHostingApiKeyDao.java | 13 + .../api/key/GameHostingApiKeyDaoWrapper.java | 44 ++ .../db/dao/api/key/PlayerApiKeyDao.java | 62 +++ .../dao/api/key/PlayerApiKeyDaoWrapper.java | 111 +++++ .../dao/api/key/PlayerApiKeyLookupRecord.java | 57 +++ .../key/PlayerIdentifiersByApiKeyLookup.java | 26 + .../dao/chat/history/LobbyChatHistoryDao.java | 34 ++ .../db/dao/lobby/games/LobbyGameDao.java | 60 +++ .../triplea/db/dao/moderator/BadWordsDao.java | 23 + .../moderator/ModeratorAuditHistoryDao.java | 80 ++++ .../ModeratorAuditHistoryRecord.java | 27 ++ .../dao/moderator/ModeratorUserDaoData.java | 21 + .../db/dao/moderator/ModeratorsDao.java | 35 ++ .../chat/history/ChatHistoryRecord.java | 34 ++ .../chat/history/GameChatHistoryDao.java | 24 + .../player/info/PlayerAliasRecord.java | 41 ++ .../player/info/PlayerBanRecord.java | 44 ++ .../info/PlayerInfoForModeratorDao.java | 48 ++ .../db/dao/temp/password/TempPasswordDao.java | 59 +++ .../temp/password/TempPasswordHistoryDao.java | 31 ++ .../org/triplea/db/dao/user/UserJdbiDao.java | 51 ++ .../db/dao/user/ban/BanLookupRecord.java | 27 ++ .../triplea/db/dao/user/ban/UserBanDao.java | 62 +++ .../db/dao/user/ban/UserBanRecord.java | 38 ++ .../db/dao/user/history/PlayerHistoryDao.java | 16 + .../dao/user/history/PlayerHistoryRecord.java | 15 + .../triplea/db/dao/user/role/UserRole.java | 20 + .../triplea/db/dao/user/role/UserRoleDao.java | 9 + .../db/dao/user/role/UserRoleLookup.java | 20 + .../db/dao/username/ban/UsernameBanDao.java | 34 ++ .../dao/username/ban/UsernameBanRecord.java | 21 + .../triplea/modules/LobbyModuleConfig.java | 18 + .../modules/chat/ChatMessagingService.java | 45 ++ .../triplea/modules/chat/ChatterSession.java | 18 + .../org/triplea/modules/chat/Chatters.java | 185 +++++++ .../event/processing/ChatMessageListener.java | 74 +++ .../processing/ChatParticipantAdapter.java | 33 ++ .../processing/PlayerConnectedListener.java | 57 +++ .../processing/PlayerIsMutedMessage.java | 37 ++ .../event/processing/PlayerLeftListener.java | 27 ++ .../chat/event/processing/SlapListener.java | 34 ++ .../processing/StatusUpdateListener.java | 34 ++ .../forgot/password/ForgotPasswordModule.java | 79 +++ .../forgot/password/PasswordEmailSender.java | 70 +++ .../forgot/password/PasswordGenerator.java | 13 + .../forgot/password/TempPasswordHistory.java | 25 + .../password/TempPasswordPersistence.java | 40 ++ .../modules/game/listing/GameListing.java | 234 +++++++++ .../game/listing/GameTtlExpiredListener.java | 18 + .../game/lobby/watcher/ChatUploadModule.java | 42 ++ .../game/lobby/watcher/ConnectivityCheck.java | 35 ++ .../game/lobby/watcher/GamePostingModule.java | 42 ++ .../access/log/AccessLogService.java | 42 ++ .../history/ModeratorAuditHistoryService.java | 30 ++ .../moderation/bad/words/BadWordsService.java | 59 +++ .../ban/name/UsernameBanService.java | 62 +++ .../ban/user/BannedPlayerEventHandler.java | 23 + .../moderation/ban/user/UserBanService.java | 164 +++++++ .../history/FetchGameChatHistoryModule.java | 26 + .../disconnect/user/DisconnectUserAction.java | 58 +++ .../moderators/ModeratorsService.java | 113 +++++ .../remote/actions/RemoteActionsModule.java | 43 ++ .../player/info/FetchPlayerInfoModule.java | 104 ++++ .../account/NameIsAvailableValidation.java | 27 ++ .../modules/user/account/NameValidation.java | 41 ++ .../user/account/PasswordBCrypter.java | 34 ++ .../user/account/create/AccountCreator.java | 39 ++ .../account/create/CreateAccountModule.java | 40 ++ .../create/CreateAccountValidation.java | 47 ++ .../user/account/create/EmailValidation.java | 15 + .../account/create/PasswordValidation.java | 22 + .../user/account/login/AccessLogUpdater.java | 31 ++ .../user/account/login/ApiKeyGenerator.java | 36 ++ .../account/login/LobbyLoginMessageDao.java | 26 + .../user/account/login/LoginModule.java | 133 +++++ .../user/account/login/LoginRecord.java | 17 + .../authorizer/anonymous/AnonymousLogin.java | 34 ++ .../authorizer/registered/PasswordCheck.java | 37 ++ .../temp/password/TempPasswordLogin.java | 44 ++ .../account/update/UpdateAccountService.java | 39 ++ .../web/socket/GameConnectionWebSocket.java | 33 ++ .../web/socket/PlayerConnectionWebSocket.java | 38 ++ .../web/socket/SessionBannedCheck.java | 24 + .../src/test/java/org/triplea/TestData.java | 25 + .../db/LobbyModuleDatabaseTestSupport.java | 12 + .../db/dao/access/log/AccessLogDaoTest.java | 123 +++++ .../dao/api/key/GameHostingApiKeyDaoTest.java | 41 ++ .../key/GameHostingApiKeyDaoWrapperTest.java | 62 +++ .../db/dao/api/key/PlayerApiKeyDaoTest.java | 134 ++++++ .../api/key/PlayerApiKeyDaoWrapperTest.java | 175 +++++++ .../api/key/PlayerApiKeyLookupRecordTest.java | 52 ++ .../chat/history/LobbyChatHistoryDaoTest.java | 31 ++ .../db/dao/lobby/games/LobbyGameDaoTest.java | 48 ++ .../db/dao/moderator/BadWordsDaoTest.java | 69 +++ .../ModeratorAuditHistoryDaoTest.java | 105 ++++ .../db/dao/moderator/ModeratorsDaoTest.java | 59 +++ .../chat/history/ChatHistoryRecordTest.java | 23 + .../chat/history/GameChatHistoryDaoTest.java | 101 ++++ .../player/info/PlayerAliasRecordTest.java | 33 ++ .../player/info/PlayerBanRecordTest.java | 37 ++ .../info/PlayerInfoForModeratorDaoTest.java | 159 ++++++ .../temp/password/TempPasswordDaoTest.java | 91 ++++ .../password/TempPasswordHistoryDaoTest.java | 44 ++ .../triplea/db/dao/user/UserJdbiDaoTest.java | 87 ++++ .../db/dao/user/ban/UserBanDaoTest.java | 139 ++++++ .../db/dao/user/role/UserRoleDaoTest.java | 32 ++ .../db/dao/user/role/UserRoleTest.java | 22 + .../dao/username/ban/UsernameBanDaoTest.java | 88 ++++ .../triplea/modules/chat/ChattersTest.java | 351 ++++++++++++++ .../processing/ChatMessageListenerTest.java | 113 +++++ .../ChatParticipantAdapterTest.java | 89 ++++ .../PlayerConnectedListenerTest.java | 156 ++++++ .../processing/PlayerIsMutedMessageTest.java | 45 ++ .../processing/PlayerLeftListenerTest.java | 67 +++ .../event/processing/SlapListenerTest.java | 84 ++++ .../processing/StatusUpdateListenerTest.java | 84 ++++ .../password/ForgotPasswordModuleTest.java | 102 ++++ .../password/PasswordGeneratorTest.java | 37 ++ .../password/TempPasswordPersistenceTest.java | 61 +++ .../modules/game/listing/GameListingTest.java | 453 ++++++++++++++++++ .../listing/GameTtlExpiredListenerTest.java | 28 ++ .../lobby/watcher/ChatUploadModuleTest.java | 59 +++ .../lobby/watcher/ConnectivityCheckTest.java | 52 ++ .../lobby/watcher/GamePostingModuleTest.java | 59 +++ .../access/log/AccessLogServiceTest.java | 71 +++ .../ModeratorAuditHistoryServiceTest.java | 65 +++ .../bad/words/BadWordsServiceTest.java | 77 +++ .../ban/name/UsernameBanServiceTest.java | 105 ++++ .../user/BannedPlayerEventHandlerTest.java | 29 ++ .../ban/user/UserBanServiceTest.java | 166 +++++++ .../FetchGameChatHistoryModuleTest.java | 60 +++ .../user/DisconnectUserActionTest.java | 135 ++++++ .../moderators/ModeratorsServiceTest.java | 156 ++++++ .../actions/RemoteActionsModuleTest.java | 86 ++++ .../info/FetchPlayerInfoModuleTest.java | 209 ++++++++ .../NameIsAvailableValidationTest.java | 40 ++ .../user/account/NameValidationTest.java | 72 +++ .../user/account/PasswordBCrypterTest.java | 33 ++ .../account/create/AccountCreatorTest.java | 41 ++ .../create/CreateAccountModuleTest.java | 64 +++ .../create/CreateAccountValidationTest.java | 103 ++++ .../account/create/EmailValidationTest.java | 22 + .../create/PasswordValidationTest.java | 26 + .../account/login/AccessLogUpdaterTest.java | 49 ++ .../login/LobbyLoginMessageDaoTest.java | 26 + .../user/account/login/LoginModuleTest.java | 254 ++++++++++ .../anonymous/AnonymousLoginTest.java | 63 +++ .../registered/PasswordCheckTest.java | 69 +++ .../temp/password/TempPasswordLoginTest.java | 71 +++ .../update/UpdateAccountServiceTest.java | 65 +++ .../web/socket/SessionBannedCheckTest.java | 39 ++ .../datasets/access_log/access_log.yml | 11 + .../access_log/access_log_post_insert.yml | 9 + .../datasets/access_log/lobby_user.yml | 6 + .../datasets/access_log/user_role.yml | 3 + .../resources/datasets/bad_words/bad_word.yml | 9 + .../bad_words/bad_word_post_insert.yml | 6 + .../bad_words/bad_word_post_remove.yml | 1 + .../game_chat_history/game_chat_history.yml | 41 ++ .../game_hosting_api_key.yml | 9 + .../datasets/game_chat_history/lobby_game.yml | 22 + .../game_hosting_api_key/insert_key_after.yml | 3 + .../insert_key_before.yml | 1 + .../game_hosting_api_key/key_exists.yml | 5 + .../lobby_api_key/delete_old_keys_after.yml | 10 + .../lobby_api_key/delete_old_keys_before.yml | 19 + .../lobby_api_key/empty_lobby_api_key.yml | 1 + .../datasets/lobby_api_key/lobby_api_key.yml | 26 + .../lobby_api_key_post_insert.yml | 8 + .../datasets/lobby_api_key/lobby_user.yml | 6 + .../datasets/lobby_api_key/user_role.yml | 9 + .../lobby_chat_history/lobby_api_key.yml | 12 + .../lobby_chat_history_post_insert.yml | 4 + .../lobby_chat_history/lobby_user.yml | 6 + .../datasets/lobby_chat_history/user_role.yml | 3 + .../game_chat_history_post_insert.yml | 4 + .../lobby_games/game_hosting_api_key.yml | 10 + .../datasets/lobby_games/lobby_game.yml | 6 + .../lobby_games/lobby_game_post_insert.yml | 4 + .../empty_moderator_action_history.yml | 1 + .../datasets/moderator_audit/lobby_user.yml | 12 + .../moderator_action_history.yml | 21 + .../moderator_action_history_post_insert.yml | 5 + .../datasets/moderator_audit/user_role.yml | 3 + .../moderator_player_lookup/access_log.yml | 31 ++ .../moderator_player_lookup/banned_user.yml | 29 ++ .../datasets/moderators/access_log.yml | 6 + .../datasets/moderators/lobby_user.yml | 16 + .../lobby_user_post_update_roles.yml | 16 + .../datasets/moderators/user_role.yml | 7 + .../datasets/temp_password/lobby_user.yml | 11 + .../temp_password/temp_password_request.yml | 11 + .../datasets/temp_password/user_role.yml | 3 + .../datasets/temp_password_history/sample.yml | 5 + .../datasets/user/change_password_after.yml | 6 + .../datasets/user/create_user_after.yml | 5 + .../datasets/user/empty_lobby_user.yml | 1 + .../resources/datasets/user/lobby_user.yml | 6 + .../datasets/user/post_change_email.yml | 6 + .../resources/datasets/user/user_role.yml | 3 + .../datasets/user_ban/add_ban_after.yml | 5 + .../datasets/user_ban/add_ban_before.yml | 1 + .../datasets/user_ban/banned_by_ip.yml | 15 + .../datasets/user_ban/lookup_bans.yml | 22 + .../user_ban/lookup_username_by_ban_id.yml | 7 + .../datasets/user_ban/remove_ban_after.yml | 1 + .../datasets/user_ban/remove_ban_before.yml | 7 + .../resources/datasets/user_role/initial.yml | 5 + .../add_banned_username_after.yml | 2 + .../add_banned_username_before.yml | 1 + .../username_ban/get_banned_usernames.yml | 5 + .../remove_banned_username_after.yml | 1 + .../remove_banned_username_before.yml | 3 + .../src/test/resources/db-cleanup.sql | 16 + .../src/test/resources/dbunit.yml | 5 + .../src/test/resources/logback-test.xml | 18 + server/server-lib/build.gradle | 64 +++ .../common/AuthenticationConfiguration.java | 48 ++ .../common/IllegalArgumentMapper.java | 17 + .../dropwizard/common/IpAddressExtractor.java | 23 + .../dropwizard/common/JdbiLogging.java | 31 ++ .../common/ServerConfiguration.java | 94 ++++ .../lib/scheduled/tasks/ScheduledTask.java | 47 ++ .../spitfire/server/HttpController.java | 17 + .../common/IpAddressExtractorTest.java | 47 ++ settings.gradle | 17 + src/main/java/org/triplea/Example.java | 13 - verify.sh | 46 +- 360 files changed, 17651 insertions(+), 17 deletions(-) create mode 100755 .docker-compose/connect-to-db.sh create mode 100644 .docker-compose/database/01-init.sql create mode 100644 .docker-compose/nginx/default.conf create mode 100644 docker-compose.yml create mode 100644 http-clients/github-client/build.gradle create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/BranchInfoResponse.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueRequest.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueResponse.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiClient.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiFeignClient.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/LatestReleaseResponse.java create mode 100644 http-clients/github-client/src/main/java/org/triplea/http/client/github/MapRepoListing.java create mode 100644 http-clients/github-client/src/test/java/org/triplea/http/client/github/GithubApiClientTest.java create mode 100644 http-clients/github-client/src/test/resources/logback-test.xml create mode 100644 http-clients/github-client/src/test/resources/sample_responses/branch_listing_response.json create mode 100644 http-clients/github-client/src/test/resources/sample_responses/latest_release_response.json create mode 100644 http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page1.json create mode 100644 http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page2.json create mode 100644 server/README-WIP.md create mode 100644 server/README.md create mode 100644 server/database-test-support/build.gradle create mode 100644 server/database-test-support/src/main/java/org/triplea/spitfire/database/DatabaseTestSupport.java create mode 100644 server/database-test-support/src/main/java/org/triplea/spitfire/database/DbRiderTestExtension.java create mode 100644 server/database/.dockerignore create mode 100644 server/database/README.md create mode 100644 server/database/build.gradle create mode 100755 server/database/connect_to_docker_db create mode 100755 server/database/load_sample_data create mode 100755 server/database/reset_docker_db create mode 100644 server/database/sql/init/01-create-users.sql create mode 100644 server/database/sql/init/02-create-databases.sql create mode 100644 server/database/sql/sample_data/lobby_db_sample_data.sql create mode 100644 server/database/src/main/resources/db/migration/error_report/V1.00.00__create_tables.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.00__lobby.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.01__add_chat_history.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.02__game_and_player_history.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.03__change_game_history_index.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.04__add_lobby_user_fk_to_access_log.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.05__drop_legacy_password.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.00.06__error_report_issue_and_title_column.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.01.00__fix_username_ban_casing.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.01.01__maps_index_tables.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.01.02__map_tags.sql create mode 100644 server/database/src/main/resources/db/migration/lobby_db/V2.02.00__lobby_message.sql create mode 100755 server/database/start_docker_db create mode 100644 server/dropwizard-server/build.gradle create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/ResponseStatus.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerApplication.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerConfig.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticator.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUser.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilter.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizer.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/latest/version/LatestVersionController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameHostingController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameListingController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/LobbyWatcherController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/PlayersInGameController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/AccessLogController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/BadWordsController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/DisconnectUserController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/GameChatHistoryController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorAuditHistoryController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorsController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/MuteUserController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/RemoteActionsController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UserBanController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UsernameBanController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/CreateAccountController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/ForgotPasswordController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/LoginController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/PlayerInfoController.java create mode 100644 server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/UpdateAccountController.java create mode 100644 server/dropwizard-server/src/main/resources/configuration.yml create mode 100644 server/dropwizard-server/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/ControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/DropwizardServerExtension.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireDatabaseTestSupport.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireServerTestExtension.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/TestData.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticatorTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUserTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizerTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/AccessLogControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/BadWordsControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/CreateAccountControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/DisconnectUserControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ForgotPasswordControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameChatHistoryControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameHostingControllerTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingControllerTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingWebsocketIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyChatIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWatcherControllerTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWebsocketClientIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LoginControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorAuditHistoryControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorsControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/MuteUserControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/PlayerInfoControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/RemoteActionsControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserAccountControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserBanControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UsernameBanControllerIntegrationTest.java create mode 100644 server/dropwizard-server/src/test/resources/datasets/access_log.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/bad_word.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/banned_user.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/banned_username.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/game_hosting_api_key.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/lobby_api_key.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/lobby_user.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/map_index.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/map_tag_value.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/moderator_action_history.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/temp_password_request.yml create mode 100644 server/dropwizard-server/src/test/resources/datasets/user_role.yml create mode 100644 server/dropwizard-server/src/test/resources/db-cleanup.sql create mode 100644 server/dropwizard-server/src/test/resources/dbunit.yml create mode 100644 server/latest-version-module/README.md create mode 100644 server/latest-version-module/build.gradle create mode 100644 server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModule.java create mode 100644 server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModuleConfig.java create mode 100644 server/lobby-module/README.md create mode 100644 server/lobby-module/build.gradle create mode 100644 server/lobby-module/src/main/java/org/triplea/db/LobbyModuleRowMappers.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/ApiKeyHasher.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapper.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapper.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerIdentifiersByApiKeyLookup.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/lobby/games/LobbyGameDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/BadWordsDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorUserDaoData.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorsDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/UserJdbiDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/BanLookupRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRole.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleLookup.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/LobbyModuleConfig.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/ChatMessagingService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/ChatterSession.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/Chatters.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatMessageListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapter.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerConnectedListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessage.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerLeftListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/SlapListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/StatusUpdateListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/forgot/password/ForgotPasswordModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordEmailSender.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordGenerator.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordHistory.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordPersistence.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameListing.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameTtlExpiredListener.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ChatUploadModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheck.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/GamePostingModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/access/log/AccessLogService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/bad/words/BadWordsService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/name/UsernameBanService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandler.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/UserBanService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserAction.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/moderators/ModeratorsService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/player/info/FetchPlayerInfoModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/NameIsAvailableValidation.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/NameValidation.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/PasswordBCrypter.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/create/AccountCreator.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountValidation.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/create/EmailValidation.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/create/PasswordValidation.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/AccessLogUpdater.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/ApiKeyGenerator.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LobbyLoginMessageDao.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginModule.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginRecord.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLogin.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheck.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLogin.java create mode 100644 server/lobby-module/src/main/java/org/triplea/modules/user/account/update/UpdateAccountService.java create mode 100644 server/lobby-module/src/main/java/org/triplea/web/socket/GameConnectionWebSocket.java create mode 100644 server/lobby-module/src/main/java/org/triplea/web/socket/PlayerConnectionWebSocket.java create mode 100644 server/lobby-module/src/main/java/org/triplea/web/socket/SessionBannedCheck.java create mode 100644 server/lobby-module/src/test/java/org/triplea/TestData.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/LobbyModuleDatabaseTestSupport.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/access/log/AccessLogDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapperTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapperTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecordTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/lobby/games/LobbyGameDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/BadWordsDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorsDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecordTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecordTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecordTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/user/UserJdbiDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/user/ban/UserBanDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/db/dao/username/ban/UsernameBanDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/ChattersTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatMessageListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapterTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerConnectedListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessageTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerLeftListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/SlapListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/StatusUpdateListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/forgot/password/ForgotPasswordModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/forgot/password/PasswordGeneratorTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/forgot/password/TempPasswordPersistenceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameListingTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameTtlExpiredListenerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ChatUploadModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheckTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/GamePostingModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/access/log/AccessLogServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/bad/words/BadWordsServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/name/UsernameBanServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandlerTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/UserBanServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserActionTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/moderators/ModeratorsServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/player/info/FetchPlayerInfoModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/NameIsAvailableValidationTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/NameValidationTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/PasswordBCrypterTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/create/AccountCreatorTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountValidationTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/create/EmailValidationTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/create/PasswordValidationTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/AccessLogUpdaterTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LobbyLoginMessageDaoTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LoginModuleTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLoginTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheckTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLoginTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/modules/user/account/update/UpdateAccountServiceTest.java create mode 100644 server/lobby-module/src/test/java/org/triplea/web/socket/SessionBannedCheckTest.java create mode 100644 server/lobby-module/src/test/resources/datasets/access_log/access_log.yml create mode 100644 server/lobby-module/src/test/resources/datasets/access_log/access_log_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/access_log/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/access_log/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/bad_words/bad_word.yml create mode 100644 server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_remove.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_chat_history/game_chat_history.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_chat_history/game_hosting_api_key.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_chat_history/lobby_game.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_before.yml create mode 100644 server/lobby-module/src/test/resources/datasets/game_hosting_api_key/key_exists.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_before.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/empty_lobby_api_key.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_api_key/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_api_key.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_chat_history_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_chat_history/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_games/game_chat_history_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_games/game_hosting_api_key.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game.yml create mode 100644 server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_audit/empty_moderator_action_history.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_audit/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history_post_insert.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_audit/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_player_lookup/access_log.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderator_player_lookup/banned_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderators/access_log.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderators/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderators/lobby_user_post_update_roles.yml create mode 100644 server/lobby-module/src/test/resources/datasets/moderators/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/temp_password/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/temp_password/temp_password_request.yml create mode 100644 server/lobby-module/src/test/resources/datasets/temp_password/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/temp_password_history/sample.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/change_password_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/create_user_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/empty_lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/lobby_user.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/post_change_email.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user/user_role.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/add_ban_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/add_ban_before.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/banned_by_ip.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/lookup_bans.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/lookup_username_by_ban_id.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_before.yml create mode 100644 server/lobby-module/src/test/resources/datasets/user_role/initial.yml create mode 100644 server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_before.yml create mode 100644 server/lobby-module/src/test/resources/datasets/username_ban/get_banned_usernames.yml create mode 100644 server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_after.yml create mode 100644 server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_before.yml create mode 100644 server/lobby-module/src/test/resources/db-cleanup.sql create mode 100644 server/lobby-module/src/test/resources/dbunit.yml create mode 100644 server/lobby-module/src/test/resources/logback-test.xml create mode 100644 server/server-lib/build.gradle create mode 100644 server/server-lib/src/main/java/org/triplea/dropwizard/common/AuthenticationConfiguration.java create mode 100644 server/server-lib/src/main/java/org/triplea/dropwizard/common/IllegalArgumentMapper.java create mode 100644 server/server-lib/src/main/java/org/triplea/dropwizard/common/IpAddressExtractor.java create mode 100644 server/server-lib/src/main/java/org/triplea/dropwizard/common/JdbiLogging.java create mode 100644 server/server-lib/src/main/java/org/triplea/dropwizard/common/ServerConfiguration.java create mode 100644 server/server-lib/src/main/java/org/triplea/server/lib/scheduled/tasks/ScheduledTask.java create mode 100644 server/server-lib/src/main/java/org/triplea/spitfire/server/HttpController.java create mode 100644 server/server-lib/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java create mode 100644 settings.gradle delete mode 100644 src/main/java/org/triplea/Example.java diff --git a/.docker-compose/connect-to-db.sh b/.docker-compose/connect-to-db.sh new file mode 100755 index 0000000..904ea99 --- /dev/null +++ b/.docker-compose/connect-to-db.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Connect to a locally running database that was started up via docker-compose. + +set -eu + +DB_IP=$(docker inspect \ + --format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ + triplea-database-1) + +export PGPASSWORD=postgres +psql -h "$DB_IP" -U postgres diff --git a/.docker-compose/database/01-init.sql b/.docker-compose/database/01-init.sql new file mode 100644 index 0000000..d63bf5c --- /dev/null +++ b/.docker-compose/database/01-init.sql @@ -0,0 +1,5 @@ +create user lobby_user password 'lobby'; +create database lobby_db owner lobby_user; + +create user error_report_user password 'error_report'; +create database error_report owner error_report_user; diff --git a/.docker-compose/nginx/default.conf b/.docker-compose/nginx/default.conf new file mode 100644 index 0000000..b37b111 --- /dev/null +++ b/.docker-compose/nginx/default.conf @@ -0,0 +1,57 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + proxy_pass http://lobby:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 90; + } + + location /game-connection/ws { + proxy_pass http://lobby:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /player-connection/ws { + proxy_pass http://lobby:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + + location /game-support { + proxy_pass http://game-support-server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 90; + } + + location /maps { + proxy_pass http://maps-server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 90; + } +} diff --git a/Dockerfile b/Dockerfile index b09b037..9e1eec4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,9 @@ FROM alpine:latest + + +#FROM openjdk:11-jre-slim-buster +# +#EXPOSE 8080 +#ADD configuration.yml / +#ADD build/libs/triplea-dropwizard-server.jar / +#CMD java -jar triplea-dropwizard-server.jar server /configuration.yml diff --git a/build.gradle b/build.gradle index 8becfaa..b6fc335 100644 --- a/build.gradle +++ b/build.gradle @@ -7,9 +7,6 @@ plugins { apply plugin: "com.diffplug.spotless" -dependencies { - implementation("triplea:lobby-client:2.6.14756") -} repositories { mavenCentral() @@ -40,3 +37,55 @@ spotless { removeUnusedImports() } } + +subprojects { + + ext { + apacheHttpComponentsVersion = "4.5.14" + awaitilityVersion = "4.2.1" + bcryptVersion = "0.10.2" + caffeineVersion = "3.1.8" + checkstyleVersion = "8.45" + commonsCliVersion = "1.8.0" + commonsCodecVersion = "1.17.0" + commonsIoVersion = "2.16.1" + commonsMathVersion = "3.6.1" + commonsTextVersion = "1.12.0" + databaseRiderVersion = "1.42.0" + dropwizardVersion = "2.1.0" + dropwizardWebsocketsVersion = "1.3.14" + equalsVerifierVersion = "3.16.1" + feignCoreVersion = "13.2.1" + feignGsonVersion = "13.2.1" + javaWebSocketVersion = "1.5.3" + gsonVersion = "2.11.0" + guavaVersion = "33.2.1-jre" + hamcrestJsonVersion = "0.3" + hamcrestOptionalVersion = "2.0.0" + hamcrestVersion = "2.0.0.0" + jacksonDataTypeVersion = "2.17.1" + jakartaMailVersion = "2.0.1" + javaWebsocketVersion = "1.5.3" + javaxActivationVersion = "1.1.1" + jaxbApiVersion = "2.3.1" + jaxbCoreVersion = "4.0.5" + jaxbImplVersion = "4.0.5" + jdbiVersion = "3.45.1" + jetbrainsAnnotationsVersion = "24.1.0" + jlayerVersion = "1.0.1.4" + junitJupiterVersion = "5.10.2" + junitPlatformLauncherVersion = "1.10.2" + logbackClassicVersion = "1.2.11" + mockitoVersion = "5.11.0" + openFeignVersion = "13.2.1" + postgresqlVersion = "42.7.3" + snakeYamlVersion = "2.7" + sonatypeGoodiesPrefsVersion = "2.3.9" + substanceVersion = "4.5.0" + wireMockJunit5Version = "1.3.1" + wireMockVersion = "3.0.1" + xchartVersion = "3.8.8" + xmlUnitCore = "2.10.0" + xmlUnitMatchers = "2.10.0" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..291eae4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +# This docker-compose file depends on './gradlew shadowJar' +# +# Launches all of the background servers used by TripleA. +# The main entrypoint to those services is NGINX which +# is listening on localhost:80 +# +version: '3' +services: + database: + image: postgres:10 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + volumes: + - ./.docker-compose/database/01-init.sql:/docker-entrypoint-initdb.d/01-init.sql + ports: + - "5432:5432" + healthcheck: + test: echo 'select 1' | psql -h localhost -U postgres | grep -q '1 row' + interval: 3s + retries: 10 + timeout: 3s + + lobby: + build: + context: spitfire-server/dropwizard-server/ + dockerfile: Dockerfile + environment: + - DATABASE_USER=lobby_user + - DATABASE_PASSWORD=lobby + - DB_URL=database:5432/lobby_db + ports: + - "8080" + depends_on: + - database + + game-support-server: + build: + context: servers/game-support/server/ + dockerfile: Dockerfile + environment: + - DB_URL=database:5432/lobby_db + ports: + - "8080" + depends_on: + - database + + maps-server: + build: + context: servers/maps/server/ + dockerfile: Dockerfile + environment: + - DB_URL=database:5432/lobby_db + ports: + - "8080" + depends_on: + - database + + nginx: + image: nginx:stable-alpine-perl + volumes: + - ./.docker-compose/nginx/default.conf:/etc/nginx/conf.d/default.conf + ports: + - "80:80" + links: + - game-support-server + - maps-server + - lobby diff --git a/http-clients/github-client/build.gradle b/http-clients/github-client/build.gradle new file mode 100644 index 0000000..699c8e8 --- /dev/null +++ b/http-clients/github-client/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "java" +} + +dependencies { + implementation "io.github.openfeign:feign-core:13.2.1" + implementation "io.github.openfeign:feign-gson:13.2.1" +// implementation project(":lib:feign-common") +// implementation project(":lib:java-extras") + testImplementation "ru.lanwen.wiremock:wiremock-junit5:1.3.1" + testImplementation "com.github.tomakehurst:wiremock:1.3.1" +// testImplementation project(":lib:test-common") +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/BranchInfoResponse.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/BranchInfoResponse.java new file mode 100644 index 0000000..f445718 --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/BranchInfoResponse.java @@ -0,0 +1,42 @@ +package org.triplea.http.client.github; + +import com.google.gson.annotations.SerializedName; +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.ToString; + +/** + * Represents the data returned by github API for their 'branches' endpoint. This class Presents a + * simplified interface for what is otherwise a JSON response. + */ +@ToString +@AllArgsConstructor +public class BranchInfoResponse { + @SerializedName("commit") + private final LastCommit lastCommit; + + /** Returns the date of the last commit. */ + public Instant getLastCommitDate() { + return Instant.parse(lastCommit.commit.commitDetails.date); + } + + @ToString + @AllArgsConstructor + private static class LastCommit { + + private final Commit commit; + + @ToString + @AllArgsConstructor + private static class Commit { + @SerializedName("author") + private final CommitDetails commitDetails; + + @ToString + @AllArgsConstructor + private static class CommitDetails { + private final String date; + } + } + } +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueRequest.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueRequest.java new file mode 100644 index 0000000..b5358cd --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueRequest.java @@ -0,0 +1,31 @@ +package org.triplea.http.client.github; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.triplea.http.client.HttpClientConstants; +import org.triplea.java.StringUtils; + +/** Represents request data to create a github issue. */ +@ToString +@EqualsAndHashCode +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateIssueRequest { + private String title; + private String body; + private String[] labels; + + public String getTitle() { + return title == null ? null : StringUtils.truncate(title, HttpClientConstants.TITLE_MAX_LENGTH); + } + + public String getBody() { + return body == null + ? null + : StringUtils.truncate(body, HttpClientConstants.REPORT_BODY_MAX_LENGTH); + } +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueResponse.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueResponse.java new file mode 100644 index 0000000..509b5fc --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/CreateIssueResponse.java @@ -0,0 +1,15 @@ +package org.triplea.http.client.github; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** Response JSON object from github after we create a new issue. */ +@ToString +@AllArgsConstructor +@Getter +public class CreateIssueResponse { + @SerializedName("html_url") + private final String htmlUrl; +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiClient.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiClient.java new file mode 100644 index 0000000..4d61fc4 --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiClient.java @@ -0,0 +1,142 @@ +package org.triplea.http.client.github; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import feign.FeignException; +import java.net.URI; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.Getter; +import org.slf4j.LoggerFactory; +import org.triplea.http.client.HttpClient; + +/** Can be used to interact with github's webservice API. */ +public class GithubApiClient { + /** If this client is set to 'test' mode, we will return a stubbed response. */ + @VisibleForTesting + static final String STUBBED_RETURN_VALUE = + "API-token==test--returned-a-stubbed-github-issue-link"; + + private final GithubApiFeignClient githubApiFeignClient; + + /** + * Flag useful for testing, when set to true no API calls will be made and a hardcoded stubbed + * value of {@code STUBBED_RETURN_VALUE} will always be returned. + */ + private final boolean stubbingModeEnabled; + + @Getter private final String org; + private final String repo; + + /** + * @param uri The URI for githubs webservice API. + * @param authToken Auth token that will be sent to Github for webservice calls. Can be empty, but + * if specified must be valid (no auth token still works, but rate limits will be more + * restrictive). + * @param org Name of the github org to be queried. + * @param repo Name of the github repository, may be left null if using only 'org' level APIs (EG: + * list repositories) + * @param stubbingModeEnabled When set to true, stub values will be returned and the github web + * API will not actually be contacted. + */ + @Builder + public GithubApiClient( + @Nonnull URI uri, + @Nonnull String authToken, + @Nonnull String org, + String repo, + final boolean stubbingModeEnabled) { + githubApiFeignClient = + HttpClient.newClient( + GithubApiFeignClient.class, + uri, + Strings.isNullOrEmpty(authToken) + ? Map.of() + : Map.of("Authorization", "token " + authToken)); + this.stubbingModeEnabled = stubbingModeEnabled; + this.org = org; + this.repo = repo; + } + + /** + * Invokes github web-API to create a github issue with the provided parameter data. + * + * @param createIssueRequest Upload data for creating the body and title of the github issue. + * @return Response from server containing link to the newly created issue. + * @throws feign.FeignException thrown on error or if non-2xx response is received + */ + public CreateIssueResponse newIssue(final CreateIssueRequest createIssueRequest) { + Preconditions.checkNotNull(repo); + if (stubbingModeEnabled) { + return new CreateIssueResponse( + STUBBED_RETURN_VALUE + String.valueOf(Math.random()).substring(0, 5)); + } + + return githubApiFeignClient.newIssue(org, repo, createIssueRequest); + } + + /** + * Returns a listing of the repositories within a github organization. This call handles paging, + * it returns a complete list and may perform multiple calls to Github. + * + *

Example equivalent cUrl call: + * + *

curl https://api.github.com/orgs/triplea-maps/repos + */ + public Collection listRepositories() { + final Collection allRepos = new HashSet<>(); + int pageNumber = 1; + Collection repos = listRepositories(pageNumber); + while (!repos.isEmpty()) { + pageNumber++; + allRepos.addAll(repos); + repos = listRepositories(pageNumber); + } + return allRepos; + } + + private Collection listRepositories(int pageNumber) { + final Map queryParams = new HashMap<>(); + queryParams.put("per_page", "100"); + queryParams.put("page", String.valueOf(pageNumber)); + + return githubApiFeignClient.listRepos(queryParams, org); + } + + /** + * Fetches details of a specific branch on a specific repo. Useful for retrieving info about the + * last commit to the repo. Note, the repo listing contains a 'last_push' date, but this method + * should be used instead as the last_push date on a repo can be for any branch (even PRs). + * + *

Example equivalent cUrl: + * https://api.github.com/repos/triplea-maps/star_wars_galactic_war/branches/master + * + * @param branch Which branch to be queried. + * @return Payload response object representing the response from Github's web API. + */ + public BranchInfoResponse fetchBranchInfo(String branch) { + return fetchBranchInfo(repo, branch); + } + + public BranchInfoResponse fetchBranchInfo(String repo, String branch) { + Preconditions.checkNotNull(repo); + return githubApiFeignClient.getBranchInfo(org, repo, branch); + } + + public Optional fetchLatestVersion() { + Preconditions.checkNotNull(repo); + try { + return Optional.of(githubApiFeignClient.getLatestRelease(org, repo).getTagName()); + } catch (final FeignException e) { + LoggerFactory.getLogger(GithubApiClient.class) + .warn("No data received from server for latest engine version", e); + return Optional.empty(); + } + } +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiFeignClient.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiFeignClient.java new file mode 100644 index 0000000..cc4e957 --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/GithubApiFeignClient.java @@ -0,0 +1,38 @@ +package org.triplea.http.client.github; + +import com.google.common.annotations.VisibleForTesting; +import feign.FeignException; +import feign.Param; +import feign.QueryMap; +import feign.RequestLine; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("InterfaceNeverImplemented") +interface GithubApiFeignClient { + + @VisibleForTesting String CREATE_ISSUE_PATH = "/repos/{org}/{repo}/issues"; + @VisibleForTesting String LIST_REPOS_PATH = "/orgs/{org}/repos"; + @VisibleForTesting String BRANCHES_PATH = "/repos/{org}/{repo}/branches/{branch}"; + @VisibleForTesting String LATEST_RELEASE_PATH = "/repos/{org}/{repo}/releases/latest"; + + /** + * Creates a new issue on github.com. + * + * @throws FeignException Thrown on non-2xx responses. + */ + @RequestLine("POST " + CREATE_ISSUE_PATH) + CreateIssueResponse newIssue( + @Param("org") String org, @Param("repo") String repo, CreateIssueRequest createIssueRequest); + + @RequestLine("GET " + LIST_REPOS_PATH) + List listRepos( + @QueryMap Map queryParams, @Param("org") String org); + + @RequestLine("GET " + BRANCHES_PATH) + BranchInfoResponse getBranchInfo( + @Param("org") String org, @Param("repo") String repo, @Param("branch") String branch); + + @RequestLine("GET " + LATEST_RELEASE_PATH) + LatestReleaseResponse getLatestRelease(@Param("org") String org, @Param("repo") String repo); +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/LatestReleaseResponse.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/LatestReleaseResponse.java new file mode 100644 index 0000000..2c7fed2 --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/LatestReleaseResponse.java @@ -0,0 +1,12 @@ +package org.triplea.http.client.github; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LatestReleaseResponse { + @SerializedName("tag_name") + String tagName; +} diff --git a/http-clients/github-client/src/main/java/org/triplea/http/client/github/MapRepoListing.java b/http-clients/github-client/src/main/java/org/triplea/http/client/github/MapRepoListing.java new file mode 100644 index 0000000..e0322bd --- /dev/null +++ b/http-clients/github-client/src/main/java/org/triplea/http/client/github/MapRepoListing.java @@ -0,0 +1,25 @@ +package org.triplea.http.client.github; + +import com.google.gson.annotations.SerializedName; +import java.net.URI; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** Response object from Github listing the details of an organization's repositories. */ +@ToString +@AllArgsConstructor +@EqualsAndHashCode +@Builder +public class MapRepoListing { + @SerializedName("html_url") + String htmlUrl; + + @Getter String name; + + public URI getUri() { + return URI.create(htmlUrl); + } +} diff --git a/http-clients/github-client/src/test/java/org/triplea/http/client/github/GithubApiClientTest.java b/http-clients/github-client/src/test/java/org/triplea/http/client/github/GithubApiClientTest.java new file mode 100644 index 0000000..a3612cc --- /dev/null +++ b/http-clients/github-client/src/test/java/org/triplea/http/client/github/GithubApiClientTest.java @@ -0,0 +1,124 @@ +package org.triplea.http.client.github; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; + +import com.github.tomakehurst.wiremock.WireMockServer; +import java.net.URI; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.test.common.TestDataFileReader; +import ru.lanwen.wiremock.ext.WiremockResolver; +import ru.lanwen.wiremock.ext.WiremockUriResolver; + +@ExtendWith({WiremockResolver.class, WiremockUriResolver.class}) +class GithubApiClientTest { + + @Test + void repoListing(@WiremockResolver.Wiremock final WireMockServer server) { + stubRepoListingResponse( + 1, + server, + TestDataFileReader.readContents("sample_responses/repo_listing_response_page1.json")); + stubRepoListingResponse( + 2, + server, + TestDataFileReader.readContents("sample_responses/repo_listing_response_page2.json")); + stubRepoListingResponse(3, server, "[]"); + + final Collection repos = + GithubApiClient.builder() + .org("example-org") + .repo("map-repo") + .uri(URI.create(server.baseUrl())) + .build() + .listRepositories(); + + assertThat(repos, hasSize(3)); + assertThat( + repos, + hasItem( + MapRepoListing.builder() + .htmlUrl("https://github.com/triplea-maps/tutorial") + .name("tutorial") + .build())); + assertThat( + repos, + hasItem( + MapRepoListing.builder() + .htmlUrl("https://github.com/triplea-maps/aa_enhanced_revised") + .name("aa_enhanced_revised") + .build())); + assertThat( + repos, + hasItem( + MapRepoListing.builder() + .htmlUrl("https://github.com/triplea-maps/roman_invasion") + .name("roman_invasion") + .build())); + } + + private void stubRepoListingResponse( + final int expectedPageNumber, final WireMockServer server, final String response) { + server.stubFor( + get("/orgs/example-org/repos?per_page=100&page=" + expectedPageNumber) + .willReturn(aResponse().withStatus(200).withBody(response))); + } + + @Test + @DisplayName("Invoke branches API and verify we can retrieve last commit date") + void branchListingResponseFetchLastCommitDate( + @WiremockResolver.Wiremock final WireMockServer server) { + final String exampleResponse = + TestDataFileReader.readContents("sample_responses/branch_listing_response.json"); + server.stubFor( + get("/repos/example-org/map-repo/branches/master") + .withHeader("Authorization", equalTo("token test-token")) + .willReturn(aResponse().withStatus(200).withBody(exampleResponse))); + + final BranchInfoResponse branchInfoResponse = + GithubApiClient.builder() + .authToken("test-token") + .org("example-org") + .repo("map-repo") + .uri(URI.create(server.baseUrl())) + .build() + .fetchBranchInfo("master"); + + final Instant expectedLastCommitDate = + LocalDateTime.of(2021, 2, 4, 19, 30, 32).atOffset(ZoneOffset.UTC).toInstant(); + assertThat(branchInfoResponse.getLastCommitDate(), is(expectedLastCommitDate)); + } + + @Test + void getLatestRelease(@WiremockResolver.Wiremock final WireMockServer server) { + final String exampleResponse = + TestDataFileReader.readContents("sample_responses/latest_release_response.json"); + server.stubFor( + get("/repos/example-org/map-repo/releases/latest") + .withHeader("Authorization", equalTo("token test-token")) + .willReturn(aResponse().withStatus(200).withBody(exampleResponse))); + + final String latestVersion = + GithubApiClient.builder() + .authToken("test-token") + .org("example-org") + .repo("map-repo") + .uri(URI.create(server.baseUrl())) + .build() + .fetchLatestVersion() + .orElseThrow(); + + assertThat(latestVersion, is("2.5.22294")); + } +} diff --git a/http-clients/github-client/src/test/resources/logback-test.xml b/http-clients/github-client/src/test/resources/logback-test.xml new file mode 100644 index 0000000..2222e34 --- /dev/null +++ b/http-clients/github-client/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + %d [%thread] %logger{36} %-5level: %msg%n + + + + + + + diff --git a/http-clients/github-client/src/test/resources/sample_responses/branch_listing_response.json b/http-clients/github-client/src/test/resources/sample_responses/branch_listing_response.json new file mode 100644 index 0000000..9460576 --- /dev/null +++ b/http-clients/github-client/src/test/resources/sample_responses/branch_listing_response.json @@ -0,0 +1,97 @@ +{ + "name": "master", + "commit": { + "sha": "acbae06cf21433af89be31e72acfee1995bf43f3", + "node_id": "MDY6Q29tbWl0NTA5MTExMzU6YWNiYWUwNmNmMjE0MzNhZjg5YmUzMWU3MmFjZmVlMTk5NWJmNDNmMw==", + "commit": { + "author": { + "name": "authorName", + "email": "email@gmail.com", + "date": "2021-02-04T19:30:32Z" + }, + "committer": { + "name": "authorName", + "email": "email@gmail.com", + "date": "2021-02-04T19:30:32Z" + }, + "message": "Add description.html file", + "tree": { + "sha": "29eba65784a2ea4da212945ecbc3f147647118ea", + "url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/git/trees/29eba65784a2ea4da212945ecbc3f147647118ea" + }, + "url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/git/commits/acbae06cf21433af89be31e72acfee1995bf43f3", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/commits/acbae06cf21433af89be31e72acfee1995bf43f3", + "html_url": "https://github.com/triplea-maps/star_wars_galactic_war/commit/acbae06cf21433af89be31e72acfee1995bf43f3", + "comments_url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/commits/acbae06cf21433af89be31e72acfee1995bf43f3/comments", + "author": { + "login": "authorName", + "id": 12397753, + "node_id": "MDQ6VXNlcjEyMzk3NzUz", + "avatar_url": "https://avatars.githubusercontent.com/u/1239?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/authorName", + "html_url": "https://github.com/authorName", + "followers_url": "https://api.github.com/users/authorName/followers", + "following_url": "https://api.github.com/users/authorName/following{/other_user}", + "gists_url": "https://api.github.com/users/authorName/gists{/gist_id}", + "starred_url": "https://api.github.com/users/authorName/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/authorName/subscriptions", + "organizations_url": "https://api.github.com/users/authorName/orgs", + "repos_url": "https://api.github.com/users/authorName/repos", + "events_url": "https://api.github.com/users/authorName/events{/privacy}", + "received_events_url": "https://api.github.com/users/authorName/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "authorName", + "id": 12390000, + "node_id": "MDQ6VXNlcjEyMzk3NzUz", + "avatar_url": "https://avatars.githubusercontent.com/u/12397753?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/authorName", + "html_url": "https://github.com/authorName", + "followers_url": "https://api.github.com/users/authorName/followers", + "following_url": "https://api.github.com/users/authorName/following{/other_user}", + "gists_url": "https://api.github.com/users/authorName/gists{/gist_id}", + "starred_url": "https://api.github.com/users/authorName/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/authorName/subscriptions", + "organizations_url": "https://api.github.com/users/authorName/orgs", + "repos_url": "https://api.github.com/users/authorName/repos", + "events_url": "https://api.github.com/users/authorName/events{/privacy}", + "received_events_url": "https://api.github.com/users/authorName/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "151fb26082cd58e5a0c997beee41011461480b9b", + "url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/commits/151fb26082cd58e5a0c997beee41011461480b9b", + "html_url": "https://github.com/triplea-maps/star_wars_galactic_war/commit/151fb26082cd58e5a0c997beee41011461480b9b" + } + ] + }, + "_links": { + "self": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/branches/master", + "html": "https://github.com/triplea-maps/star_wars_galactic_war/tree/master" + }, + "protected": false, + "protection": { + "enabled": false, + "required_status_checks": { + "enforcement_level": "off", + "contexts": [ + + ] + } + }, + "protection_url": "https://api.github.com/repos/triplea-maps/star_wars_galactic_war/branches/master/protection" +} diff --git a/http-clients/github-client/src/test/resources/sample_responses/latest_release_response.json b/http-clients/github-client/src/test/resources/sample_responses/latest_release_response.json new file mode 100644 index 0000000..7f6781a --- /dev/null +++ b/http-clients/github-client/src/test/resources/sample_responses/latest_release_response.json @@ -0,0 +1,346 @@ +{ + "url": "https://api.github.com/repos/triplea-game/triplea/releases/33335898", + "assets_url": "https://api.github.com/repos/triplea-game/triplea/releases/33335898/assets", + "upload_url": "https://uploads.github.com/repos/triplea-game/triplea/releases/33335898/assets{?name,label}", + "html_url": "https://github.com/triplea-game/triplea/releases/tag/2.5.22294", + "id": 33335898, + "author": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "MDc6UmVsZWFzZTMzMzM1ODk4", + "tag_name": "2.5.22294", + "target_commitish": "f516043a9d4ae146de4a6305165ce3a686a92a32", + "name": "", + "draft": false, + "prerelease": false, + "created_at": "2020-11-02T05:11:01Z", + "published_at": "2020-11-02T05:12:47Z", + "assets": [ + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820580", + "id": 27820580, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTgw", + "name": "migrations.zip", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 7420, + "download_count": 66, + "created_at": "2020-11-02T05:12:44Z", + "updated_at": "2020-11-02T05:12:45Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/migrations.zip" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820570", + "id": 27820570, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTcw", + "name": "triplea-game-headed-2.5.22294.zip", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 26094498, + "download_count": 223, + "created_at": "2020-11-02T05:12:25Z", + "updated_at": "2020-11-02T05:12:26Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/triplea-game-headed-2.5.22294.zip" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820575", + "id": 27820575, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTc1", + "name": "triplea-game-headless-2.5.22294.zip", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 18797934, + "download_count": 105, + "created_at": "2020-11-02T05:12:38Z", + "updated_at": "2020-11-02T05:12:41Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/triplea-game-headless-2.5.22294.zip" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820573", + "id": 27820573, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTcz", + "name": "triplea-lobby-server-2.5.22294.zip", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 23701788, + "download_count": 55, + "created_at": "2020-11-02T05:12:35Z", + "updated_at": "2020-11-02T05:12:38Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/triplea-lobby-server-2.5.22294.zip" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820582", + "id": 27820582, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTgy", + "name": "triplea-maps-server-2.5.22294.zip", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 21665863, + "download_count": 47, + "created_at": "2020-11-02T05:12:46Z", + "updated_at": "2020-11-02T05:12:46Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/triplea-maps-server-2.5.22294.zip" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820572", + "id": 27820572, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTcy", + "name": "TripleA_2.5.22294_macos.dmg", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/x-apple-diskimage", + "state": "uploaded", + "size": 58909491, + "download_count": 6229, + "created_at": "2020-11-02T05:12:33Z", + "updated_at": "2020-11-02T05:12:34Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/TripleA_2.5.22294_macos.dmg" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820571", + "id": 27820571, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTcx", + "name": "TripleA_2.5.22294_unix.sh", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/x-sh", + "state": "uploaded", + "size": 64495298, + "download_count": 3696, + "created_at": "2020-11-02T05:12:28Z", + "updated_at": "2020-11-02T05:12:32Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/TripleA_2.5.22294_unix.sh" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820567", + "id": 27820567, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTY3", + "name": "TripleA_2.5.22294_windows-32bit.exe", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/x-ms-dos-executable", + "state": "uploaded", + "size": 64250880, + "download_count": 2227, + "created_at": "2020-11-02T05:12:22Z", + "updated_at": "2020-11-02T05:12:24Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/TripleA_2.5.22294_windows-32bit.exe" + }, + { + "url": "https://api.github.com/repos/triplea-game/triplea/releases/assets/27820579", + "id": 27820579, + "node_id": "MDEyOlJlbGVhc2VBc3NldDI3ODIwNTc5", + "name": "TripleA_2.5.22294_windows-64bit.exe", + "label": "", + "uploader": { + "login": "tripleabuilderbot", + "id": 14932076, + "node_id": "MDQ6VXNlcjE0OTMyMDc2", + "avatar_url": "https://avatars.githubusercontent.com/u/14932076?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/tripleabuilderbot", + "html_url": "https://github.com/tripleabuilderbot", + "followers_url": "https://api.github.com/users/tripleabuilderbot/followers", + "following_url": "https://api.github.com/users/tripleabuilderbot/following{/other_user}", + "gists_url": "https://api.github.com/users/tripleabuilderbot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/tripleabuilderbot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/tripleabuilderbot/subscriptions", + "organizations_url": "https://api.github.com/users/tripleabuilderbot/orgs", + "repos_url": "https://api.github.com/users/tripleabuilderbot/repos", + "events_url": "https://api.github.com/users/tripleabuilderbot/events{/privacy}", + "received_events_url": "https://api.github.com/users/tripleabuilderbot/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/x-ms-dos-executable", + "state": "uploaded", + "size": 63516160, + "download_count": 30564, + "created_at": "2020-11-02T05:12:42Z", + "updated_at": "2020-11-02T05:12:44Z", + "browser_download_url": "https://github.com/triplea-game/triplea/releases/download/2.5.22294/TripleA_2.5.22294_windows-64bit.exe" + } + ], + "tarball_url": "https://api.github.com/repos/triplea-game/triplea/tarball/2.5.22294", + "zipball_url": "https://api.github.com/repos/triplea-game/triplea/zipball/2.5.22294", + "body": "" +} diff --git a/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page1.json b/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page1.json new file mode 100644 index 0000000..c7e33d3 --- /dev/null +++ b/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page1.json @@ -0,0 +1,200 @@ +[ + { + "id": 43531223, + "node_id": "MDEwOlJlcG9zaXRvcnk0MzUzMTIyMw==", + "name": "tutorial", + "full_name": "triplea-maps/tutorial", + "private": false, + "owner": { + "login": "triplea-maps", + "id": 14303309, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MzAzMzA5", + "avatar_url": "https://avatars.githubusercontent.com/u/14303309?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/triplea-maps", + "html_url": "https://github.com/triplea-maps", + "followers_url": "https://api.github.com/users/triplea-maps/followers", + "following_url": "https://api.github.com/users/triplea-maps/following{/other_user}", + "gists_url": "https://api.github.com/users/triplea-maps/gists{/gist_id}", + "starred_url": "https://api.github.com/users/triplea-maps/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/triplea-maps/subscriptions", + "organizations_url": "https://api.github.com/users/triplea-maps/orgs", + "repos_url": "https://api.github.com/users/triplea-maps/repos", + "events_url": "https://api.github.com/users/triplea-maps/events{/privacy}", + "received_events_url": "https://api.github.com/users/triplea-maps/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/triplea-maps/tutorial", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/triplea-maps/tutorial", + "forks_url": "https://api.github.com/repos/triplea-maps/tutorial/forks", + "keys_url": "https://api.github.com/repos/triplea-maps/tutorial/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/triplea-maps/tutorial/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/triplea-maps/tutorial/teams", + "hooks_url": "https://api.github.com/repos/triplea-maps/tutorial/hooks", + "issue_events_url": "https://api.github.com/repos/triplea-maps/tutorial/issues/events{/number}", + "events_url": "https://api.github.com/repos/triplea-maps/tutorial/events", + "assignees_url": "https://api.github.com/repos/triplea-maps/tutorial/assignees{/user}", + "branches_url": "https://api.github.com/repos/triplea-maps/tutorial/branches{/branch}", + "tags_url": "https://api.github.com/repos/triplea-maps/tutorial/tags", + "blobs_url": "https://api.github.com/repos/triplea-maps/tutorial/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/triplea-maps/tutorial/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/triplea-maps/tutorial/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/triplea-maps/tutorial/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/triplea-maps/tutorial/statuses/{sha}", + "languages_url": "https://api.github.com/repos/triplea-maps/tutorial/languages", + "stargazers_url": "https://api.github.com/repos/triplea-maps/tutorial/stargazers", + "contributors_url": "https://api.github.com/repos/triplea-maps/tutorial/contributors", + "subscribers_url": "https://api.github.com/repos/triplea-maps/tutorial/subscribers", + "subscription_url": "https://api.github.com/repos/triplea-maps/tutorial/subscription", + "commits_url": "https://api.github.com/repos/triplea-maps/tutorial/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/triplea-maps/tutorial/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/triplea-maps/tutorial/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/triplea-maps/tutorial/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/triplea-maps/tutorial/contents/{+path}", + "compare_url": "https://api.github.com/repos/triplea-maps/tutorial/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/triplea-maps/tutorial/merges", + "archive_url": "https://api.github.com/repos/triplea-maps/tutorial/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/triplea-maps/tutorial/downloads", + "issues_url": "https://api.github.com/repos/triplea-maps/tutorial/issues{/number}", + "pulls_url": "https://api.github.com/repos/triplea-maps/tutorial/pulls{/number}", + "milestones_url": "https://api.github.com/repos/triplea-maps/tutorial/milestones{/number}", + "notifications_url": "https://api.github.com/repos/triplea-maps/tutorial/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/triplea-maps/tutorial/labels{/name}", + "releases_url": "https://api.github.com/repos/triplea-maps/tutorial/releases{/id}", + "deployments_url": "https://api.github.com/repos/triplea-maps/tutorial/deployments", + "created_at": "2015-10-02T01:54:08Z", + "updated_at": "2021-02-04T19:31:35Z", + "pushed_at": "2021-02-04T19:31:31Z", + "git_url": "git://github.com/triplea-maps/tutorial.git", + "ssh_url": "git@github.com:triplea-maps/tutorial.git", + "clone_url": "https://github.com/triplea-maps/tutorial.git", + "svn_url": "https://github.com/triplea-maps/tutorial", + "homepage": null, + "size": 45829, + "stargazers_count": 0, + "watchers_count": 0, + "language": "HTML", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 1, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "permissions": { + "admin": false, + "push": false, + "pull": true + } + }, + { + "id": 50909601, + "node_id": "MDEwOlJlcG9zaXRvcnk1MDkwOTYwMQ==", + "name": "aa_enhanced_revised", + "full_name": "triplea-maps/aa_enhanced_revised", + "private": false, + "owner": { + "login": "triplea-maps", + "id": 14303309, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MzAzMzA5", + "avatar_url": "https://avatars.githubusercontent.com/u/14303309?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/triplea-maps", + "html_url": "https://github.com/triplea-maps", + "followers_url": "https://api.github.com/users/triplea-maps/followers", + "following_url": "https://api.github.com/users/triplea-maps/following{/other_user}", + "gists_url": "https://api.github.com/users/triplea-maps/gists{/gist_id}", + "starred_url": "https://api.github.com/users/triplea-maps/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/triplea-maps/subscriptions", + "organizations_url": "https://api.github.com/users/triplea-maps/orgs", + "repos_url": "https://api.github.com/users/triplea-maps/repos", + "events_url": "https://api.github.com/users/triplea-maps/events{/privacy}", + "received_events_url": "https://api.github.com/users/triplea-maps/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/triplea-maps/aa_enhanced_revised", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised", + "forks_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/forks", + "keys_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/teams", + "hooks_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/hooks", + "issue_events_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/issues/events{/number}", + "events_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/events", + "assignees_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/assignees{/user}", + "branches_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/branches{/branch}", + "tags_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/tags", + "blobs_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/statuses/{sha}", + "languages_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/languages", + "stargazers_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/stargazers", + "contributors_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/contributors", + "subscribers_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/subscribers", + "subscription_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/subscription", + "commits_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/contents/{+path}", + "compare_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/merges", + "archive_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/downloads", + "issues_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/issues{/number}", + "pulls_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/pulls{/number}", + "milestones_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/milestones{/number}", + "notifications_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/labels{/name}", + "releases_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/releases{/id}", + "deployments_url": "https://api.github.com/repos/triplea-maps/aa_enhanced_revised/deployments", + "created_at": "2016-02-02T09:33:26Z", + "updated_at": "2016-02-02T09:33:26Z", + "pushed_at": "2017-12-14T04:37:33Z", + "git_url": "git://github.com/triplea-maps/aa_enhanced_revised.git", + "ssh_url": "git@github.com:triplea-maps/aa_enhanced_revised.git", + "clone_url": "https://github.com/triplea-maps/aa_enhanced_revised.git", + "svn_url": "https://github.com/triplea-maps/aa_enhanced_revised", + "homepage": null, + "size": 380, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "permissions": { + "admin": false, + "push": false, + "pull": true + } + } +] diff --git a/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page2.json b/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page2.json new file mode 100644 index 0000000..01ca1b9 --- /dev/null +++ b/http-clients/github-client/src/test/resources/sample_responses/repo_listing_response_page2.json @@ -0,0 +1,101 @@ +[ + { + "id": 50909644, + "node_id": "MDEwOlJlcG9zaXRvcnk1MDkwOTY0NA==", + "name": "roman_invasion", + "full_name": "triplea-maps/roman_invasion", + "private": false, + "owner": { + "login": "triplea-maps", + "id": 14303309, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0MzAzMzA5", + "avatar_url": "https://avatars.githubusercontent.com/u/14303309?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/triplea-maps", + "html_url": "https://github.com/triplea-maps", + "followers_url": "https://api.github.com/users/triplea-maps/followers", + "following_url": "https://api.github.com/users/triplea-maps/following{/other_user}", + "gists_url": "https://api.github.com/users/triplea-maps/gists{/gist_id}", + "starred_url": "https://api.github.com/users/triplea-maps/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/triplea-maps/subscriptions", + "organizations_url": "https://api.github.com/users/triplea-maps/orgs", + "repos_url": "https://api.github.com/users/triplea-maps/repos", + "events_url": "https://api.github.com/users/triplea-maps/events{/privacy}", + "received_events_url": "https://api.github.com/users/triplea-maps/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/triplea-maps/roman_invasion", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/triplea-maps/roman_invasion", + "forks_url": "https://api.github.com/repos/triplea-maps/roman_invasion/forks", + "keys_url": "https://api.github.com/repos/triplea-maps/roman_invasion/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/triplea-maps/roman_invasion/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/triplea-maps/roman_invasion/teams", + "hooks_url": "https://api.github.com/repos/triplea-maps/roman_invasion/hooks", + "issue_events_url": "https://api.github.com/repos/triplea-maps/roman_invasion/issues/events{/number}", + "events_url": "https://api.github.com/repos/triplea-maps/roman_invasion/events", + "assignees_url": "https://api.github.com/repos/triplea-maps/roman_invasion/assignees{/user}", + "branches_url": "https://api.github.com/repos/triplea-maps/roman_invasion/branches{/branch}", + "tags_url": "https://api.github.com/repos/triplea-maps/roman_invasion/tags", + "blobs_url": "https://api.github.com/repos/triplea-maps/roman_invasion/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/triplea-maps/roman_invasion/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/triplea-maps/roman_invasion/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/triplea-maps/roman_invasion/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/triplea-maps/roman_invasion/statuses/{sha}", + "languages_url": "https://api.github.com/repos/triplea-maps/roman_invasion/languages", + "stargazers_url": "https://api.github.com/repos/triplea-maps/roman_invasion/stargazers", + "contributors_url": "https://api.github.com/repos/triplea-maps/roman_invasion/contributors", + "subscribers_url": "https://api.github.com/repos/triplea-maps/roman_invasion/subscribers", + "subscription_url": "https://api.github.com/repos/triplea-maps/roman_invasion/subscription", + "commits_url": "https://api.github.com/repos/triplea-maps/roman_invasion/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/triplea-maps/roman_invasion/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/triplea-maps/roman_invasion/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/triplea-maps/roman_invasion/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/triplea-maps/roman_invasion/contents/{+path}", + "compare_url": "https://api.github.com/repos/triplea-maps/roman_invasion/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/triplea-maps/roman_invasion/merges", + "archive_url": "https://api.github.com/repos/triplea-maps/roman_invasion/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/triplea-maps/roman_invasion/downloads", + "issues_url": "https://api.github.com/repos/triplea-maps/roman_invasion/issues{/number}", + "pulls_url": "https://api.github.com/repos/triplea-maps/roman_invasion/pulls{/number}", + "milestones_url": "https://api.github.com/repos/triplea-maps/roman_invasion/milestones{/number}", + "notifications_url": "https://api.github.com/repos/triplea-maps/roman_invasion/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/triplea-maps/roman_invasion/labels{/name}", + "releases_url": "https://api.github.com/repos/triplea-maps/roman_invasion/releases{/id}", + "deployments_url": "https://api.github.com/repos/triplea-maps/roman_invasion/deployments", + "created_at": "2016-02-02T09:33:59Z", + "updated_at": "2016-02-02T09:33:59Z", + "pushed_at": "2017-12-14T04:28:01Z", + "git_url": "git://github.com/triplea-maps/roman_invasion.git", + "ssh_url": "git@github.com:triplea-maps/roman_invasion.git", + "clone_url": "https://github.com/triplea-maps/roman_invasion.git", + "svn_url": "https://github.com/triplea-maps/roman_invasion", + "homepage": null, + "size": 512, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "permissions": { + "admin": false, + "push": false, + "pull": true + } + } +] diff --git a/server/README-WIP.md b/server/README-WIP.md new file mode 100644 index 0000000..855c4c3 --- /dev/null +++ b/server/README-WIP.md @@ -0,0 +1,12 @@ +/servers folder is a WIP + +The folder will contain a subdirectory for each server that we will be deploying. + +Currently we have things bundled as largely just one server. + +This folder is not yet used, it is a start of an effort to break this up and re-organize +the code a bit to be yet more modular. + +Further, with very specific sub-folders and more executables, we will have more opportunity +to do continuous deployments on any update without having to worry for example about +disconnecting everyone from chat. diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..5ca91fa --- /dev/null +++ b/server/README.md @@ -0,0 +1,26 @@ +## Lobby Server + +### Background + +Lobby-Server is a 'new' server to host lobby and other functionalities. Historically this was +powered by a pure java stack that used java sockets (NIO). The java server was written very early +in the project, mid-2000s, the 'http-server' allows for a modern (2019) server to be used. +The modern server has integration with JDBI, annotation based rate limiting, authentication and +affords an opportunity to rewrite the lobby server in a simpler and more modular fashion. + +### Authentication + +Connecting to the server, on success, will issue an API key back to the client. Subsequent +interactions from the client with the server will send the API key to server for further +authorization. Keep in mind all endpoints are publicly available. + +### Communication Directions - Http to Server & Websocket to Client + +Communication to server is done via standard Http endpoints. Server will process these messages +triggering event listeners that will communicate back to clients via websocket. + +### Keep-Alive + +This concept is to avoid 'ghosts' when we fail to process a disconnect. Players and connected games +will need to send HTTP requests to a keep-alive endpoint to explicitly 'register' their liveness. When +these messages are not received after a cut-off period, then the game or player are removed. diff --git a/server/database-test-support/build.gradle b/server/database-test-support/build.gradle new file mode 100644 index 0000000..fce41d2 --- /dev/null +++ b/server/database-test-support/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "java" +} + +dependencies { + implementation "com.github.database-rider:rider-junit5:$databaseRiderVersion" + implementation "org.jdbi:jdbi3-core:$jdbiVersion" + implementation "org.jdbi:jdbi3-sqlobject:$jdbiVersion" + implementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion" +} diff --git a/server/database-test-support/src/main/java/org/triplea/spitfire/database/DatabaseTestSupport.java b/server/database-test-support/src/main/java/org/triplea/spitfire/database/DatabaseTestSupport.java new file mode 100644 index 0000000..8e66fc1 --- /dev/null +++ b/server/database-test-support/src/main/java/org/triplea/spitfire/database/DatabaseTestSupport.java @@ -0,0 +1,18 @@ +package org.triplea.spitfire.database; + +public abstract class DatabaseTestSupport extends DbRiderTestExtension { + @Override + protected String getDatabaseUser() { + return "lobby_user"; + } + + @Override + protected String getDatabasePassword() { + return "lobby"; + } + + @Override + protected String getDatabaseUrl() { + return "jdbc:postgresql://localhost:5432/lobby_db"; + } +} diff --git a/server/database-test-support/src/main/java/org/triplea/spitfire/database/DbRiderTestExtension.java b/server/database-test-support/src/main/java/org/triplea/spitfire/database/DbRiderTestExtension.java new file mode 100644 index 0000000..4d089dd --- /dev/null +++ b/server/database-test-support/src/main/java/org/triplea/spitfire/database/DbRiderTestExtension.java @@ -0,0 +1,101 @@ +package org.triplea.spitfire.database; + +import com.github.database.rider.junit5.DBUnitExtension; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.mapper.RowMapperFactory; +import org.jdbi.v3.sqlobject.SqlObjectPlugin; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * Extend this class to more easily test database with DbRider. Once extended, a test can then use + * DbRider '@DataSet' annotations to inject data into database. After each test executes, the + * database is wiped clean. + * + *

Second, this extension will constructor or test method parameter inject any classes that can + * be instantiated via a 'Jdbi.onDemand(class)' or 'new DaoClass(jdbi)'. + */ +@ExtendWith(DBUnitExtension.class) +public abstract class DbRiderTestExtension + implements BeforeAllCallback, BeforeEachCallback, ParameterResolver { + + private static Jdbi jdbi; + + protected abstract String getDatabaseUser(); + + protected abstract String getDatabasePassword(); + + protected abstract String getDatabaseUrl(); + + /** Return all row mappers that should be registered with JDBI. */ + protected abstract Collection rowMappers(); + + @Override + public void beforeAll(final ExtensionContext context) { + if (jdbi == null) { + jdbi = Jdbi.create(getDatabaseUrl(), getDatabaseUser(), getDatabasePassword()); + jdbi.installPlugin(new SqlObjectPlugin()); + rowMappers().forEach(jdbi::registerRowMapper); + } + } + + @Override + public void beforeEach(final ExtensionContext context) throws Exception { + final URL cleanupFileUrl = getClass().getClassLoader().getResource("db-cleanup.sql"); + if (cleanupFileUrl != null) { + final String cleanupSql = Files.readString(Path.of(cleanupFileUrl.toURI())); + jdbi.withHandle(handle -> handle.execute(cleanupSql)); + } + } + + @Override + public boolean supportsParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + + // check if there is a one-arg (JDBI) constructor + try { + parameterContext.getParameter().getType().getConstructor(Jdbi.class); + return true; + } catch (final NoSuchMethodException e) { + // no-op, object is constructed potentially another way + } + try { + jdbi.onDemand(parameterContext.getParameter().getType()); + return true; + } catch (final IllegalArgumentException ignored) { + return false; + } + } + + @Override + public Object resolveParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + + // try to create the class using constructor that accepts one Jdbi + try { + final Constructor constructor = + parameterContext.getParameter().getType().getConstructor(Jdbi.class); + return constructor.newInstance(jdbi); + } catch (final NoSuchMethodException + | InvocationTargetException + | IllegalAccessException + | InstantiationException e) { + // no-op, object is constructed via 'jdbi.onDemand' + } + + return jdbi.onDemand(parameterContext.getParameter().getType()); + } +} diff --git a/server/database/.dockerignore b/server/database/.dockerignore new file mode 100644 index 0000000..7a8b76c --- /dev/null +++ b/server/database/.dockerignore @@ -0,0 +1,2 @@ +* +!src/ diff --git a/server/database/README.md b/server/database/README.md new file mode 100644 index 0000000..8f8719b --- /dev/null +++ b/server/database/README.md @@ -0,0 +1,24 @@ +# Database + +Hosts database migrations and docker to run a database locally. + +Stores: + - users + - lobby chat history + - user ban information + - moderator audit logs + - bug report history and rate limits + - uploaded map information + +For more information see: [database documentation](/docs/development/database/) + +## Working with database locally + +- install docker +- run: `./spitfire-server/database/start_docker_db` +- connect to DB with: `./spitfire-server/database/connect_to_docker_db` + +## Example data + +The example data inserted into a local docker will create an admin user +named "test" with password "test". diff --git a/server/database/build.gradle b/server/database/build.gradle new file mode 100644 index 0000000..6c88120 --- /dev/null +++ b/server/database/build.gradle @@ -0,0 +1,40 @@ +import org.flywaydb.gradle.task.* + +buildscript { + dependencies { + classpath "org.postgresql:postgresql:$postgresqlVersion" + } +} + +plugins { + id "org.flywaydb.flyway" version "9.22.3" +} + +flyway { + driver = "org.postgresql.Driver" +} + +task flywayMigrateLobbyDb(type: FlywayMigrateTask) { + url= "jdbc:postgresql://localhost:5432/lobby_db" + user = "lobby_user" + password = "lobby" + locations = ["filesystem:src/main/resources/db/migration/lobby_db"] +} + +task flywayMigrateErrorReportDb(type: FlywayMigrateTask) { + url= "jdbc:postgresql://localhost:5432/error_report" + user = "error_report_user" + password = "error_report" + locations = ["filesystem:src/main/resources/db/migration/error_report"] +} + +task portableInstaller(type: Zip, group: "release") { + from "src/main/resources/db/migration/" + archiveFileName = "migrations.zip" +} + +task release(group: "release", dependsOn: portableInstaller) { + doLast { + publishArtifacts(portableInstaller.outputs.files) + } +} diff --git a/server/database/connect_to_docker_db b/server/database/connect_to_docker_db new file mode 100755 index 0000000..69b3697 --- /dev/null +++ b/server/database/connect_to_docker_db @@ -0,0 +1,9 @@ +#!/bin/bash + +# Simple helper script to connect to a DB running locally on docker. +export PGPASSWORD=postgres +psql -h localhost -U postgres + +# after connecting, use "\l" to print the list of database +# Use "\c " to connect to a database +# After connecting to a database, use "\d" to list the tables in the current database diff --git a/server/database/load_sample_data b/server/database/load_sample_data new file mode 100755 index 0000000..25f6ec0 --- /dev/null +++ b/server/database/load_sample_data @@ -0,0 +1,11 @@ +#!/bin/bash + +bold="\e[1m" +bold_green="${bold}\e[32m" +normal="\e[0m" + +echo -e "[${bold_green}Inserting sample data${normal}]" + +export PGPASSWORD=postgres +psql -h localhost -U postgres lobby_db \ + < "$(dirname "$0")/sql/sample_data/lobby_db_sample_data.sql" diff --git a/server/database/reset_docker_db b/server/database/reset_docker_db new file mode 100755 index 0000000..61c638a --- /dev/null +++ b/server/database/reset_docker_db @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +PSQL="psql -h localhost -U postgres" + +export PGPASSWORD=postgres + +# Check if we can connect to database +if ! echo 'select 1' \ + | $PSQL 2> /dev/null \ + | grep -q '1 row'; +then + echo "ERROR: docker not running, start the database first" + exit 1 +fi + +echo "Force killing open connections to database" +echo "select pg_terminate_backend(pid) from pg_stat_activity where datname='lobby_db';" | $PSQL +echo "drop database lobby_db" | $PSQL + +scriptDir="$(dirname "$0")" +$PSQL < "$scriptDir/sql/init/02-create-databases.sql" + +echo "Deploying schema" +"$scriptDir/../../gradlew" flywayMigrateLobbyDb + +"$scriptDir/load_sample_data" diff --git a/server/database/sql/init/01-create-users.sql b/server/database/sql/init/01-create-users.sql new file mode 100644 index 0000000..5e67ff9 --- /dev/null +++ b/server/database/sql/init/01-create-users.sql @@ -0,0 +1,2 @@ +create user lobby_user password 'lobby'; +create user error_report_user password 'error_report'; diff --git a/server/database/sql/init/02-create-databases.sql b/server/database/sql/init/02-create-databases.sql new file mode 100644 index 0000000..508136b --- /dev/null +++ b/server/database/sql/init/02-create-databases.sql @@ -0,0 +1,2 @@ +create database lobby_db owner lobby_user; +create database error_report owner error_report_user; diff --git a/server/database/sql/sample_data/lobby_db_sample_data.sql b/server/database/sql/sample_data/lobby_db_sample_data.sql new file mode 100644 index 0000000..4b08dcb --- /dev/null +++ b/server/database/sql/sample_data/lobby_db_sample_data.sql @@ -0,0 +1,115 @@ +delete +from temp_password_request; +delete +from moderator_action_history; +delete +from lobby_api_key; +delete +from game_hosting_api_key; +delete +from lobby_user; +delete +from access_log; +delete +from user_role; +delete +from map_tag_value; +delete +from map_index; +delete +from map_tag_allowed_value; +delete +from map_tag; + + +insert into user_role(id, name) +values (1, 'ADMIN'), -- user can add/remove admins and add/remove moderators, has boot/ban privileges + (2, 'MODERATOR'), -- user has boot/ban privileges + (3, 'PLAYER'), -- standard registered user + (4, 'ANONYMOUS'), -- users that are not registered, they do not have an entry in lobby_user table + (5, 'HOST'); -- AKA LobbyWatcher, special connection for hosts to send game updates to lobby + +insert into lobby_user(id, username, email, user_role_id, bcrypt_password) +values (1000, 'test', 'email@email.com', (select id from user_role where name = 'ADMIN'), + '$2a$10$Ut3tvElEhPPr4s5wPd4dFuOvY25fa4r5XH3T7ucFTr5gJsotZl5d6'), -- password = 'test' + (1001, 'user1', 'email@email.com', (select id from user_role where name = 'PLAYER'), + '$2a$10$C4rHfjK/seKexc6KlyknP.oFVBZ7Wi.kp91qUQFgmkKajwgczXzcS'); + +insert into access_log(access_time, username, ip, system_id, lobby_user_id) +values (now() - interval '1 days', 'user1', '1.1.1.1', 'system-id1', null), + (now() - interval '2 days', 'user2', '1.1.1.2', 'system-id2', null), + (now() - interval '3 days', 'user3', '1.1.1.3', 'system-id3', null), + (now() - interval '4 days', 'user1', '1.1.1.2', 'system-id4', null), + (now() - interval '5 days', 'user2', '1.1.1.4', 'system-id5', null); + +insert into moderator_action_history(lobby_user_id, action_name, action_target) +values (1000, 'ACTION_1', 'TARGET_1'), + (1000, 'ACTION_2', 'TARGET_2'), + (1000, 'ACTION_3', 'TARGET_3'), + (1000, 'ACTION_4', 'TARGET_4'), + (1000, 'ACTION_5', 'TARGET_5'), + (1000, 'ACTION_6', 'TARGET_6'), + (1000, 'ACTION_7', 'TARGET_7'), + (1000, 'ACTION_8', 'TARGET_8'), + (1000, 'ACTION_9', 'TARGET_9'), + (1000, 'ACTION_10', 'TARGET_10'), + (1000, 'ACTION_11', 'TARGET_11'), + (1000, 'ACTION_12', 'TARGET_12'), + (1000, 'ACTION_13', 'TARGET_13'), + (1000, 'ACTION_14', 'TARGET_14'), + (1000, 'ACTION_15', 'TARGET_15'), + (1000, 'ACTION_16', 'TARGET_16'), + (1000, 'ACTION_17', 'TARGET_17'), + (1000, 'ACTION_18', 'TARGET_18'), + (1000, 'ACTION_19', 'TARGET_19'), + (1000, 'ACTION_20', 'TARGET_20'), + (1000, 'ACTION_21', 'TARGET_21'), + (1000, 'ACTION_22', 'TARGET_22'), + (1000, 'ACTION_23', 'TARGET_23'), + (1000, 'ACTION_24', 'TARGET_24'), + (1000, 'ACTION_25', 'TARGET_25'), + (1000, 'ACTION_26', 'TARGET_26'), + (1000, 'ACTION_27', 'TARGET_27'), + (1000, 'ACTION_28', 'TARGET_28'), + (1000, 'ACTION_29', 'TARGET_29'), + (1000, 'ACTION_30', 'TARGET_30'), + (1000, 'ACTION_31', 'TARGET_31'), + (1000, 'ACTION_32', 'TARGET_32'), + (1000, 'ACTION_33', 'TARGET_33'), + (1000, 'ACTION_34', 'TARGET_34'), + (1000, 'ACTION_35', 'TARGET_35'), + (1000, 'ACTION_36', 'TARGET_36'), + (1000, 'ACTION_37', 'TARGET_37'), + (1000, 'ACTION_38', 'TARGET_38'), + (1000, 'ACTION_39', 'TARGET_39'), + (1000, 'ACTION_40', 'TARGET_40'), + (1000, 'ACTION_41', 'TARGET_41'), + (1000, 'ACTION_42', 'TARGET_42'), + (1000, 'ACTION_43', 'TARGET_43'), + (1000, 'ACTION_44', 'TARGET_44'), + (1000, 'ACTION_45', 'TARGET_45'), + (1000, 'ACTION_46', 'TARGET_46'), + (1000, 'ACTION_47', 'TARGET_47'), + (1000, 'ACTION_48', 'TARGET_48'), + (1000, 'ACTION_49', 'TARGET_49'), + (1000, 'ACTION_50', 'TARGET_50'); + +insert into map_index +(id, map_name, last_commit_date, repo_url, + description, download_size_bytes, download_url, + preview_image_url) +values (1, 'test-map', now() - interval '1 days', 'http://repo-url', + 'map description', 1024, 'http://download-url', + 'http://preview-image'); + +insert into map_tag (id, name, display_order) +values (1000, 'Rating', 1), + (2000, 'Category', 2); + +insert into map_tag_allowed_value (id, map_tag_id, value) +values (300, 1000, '1'), + (301, 1000, '2'), + (302, 1000, '3'), + (400, 2000, 'Complete'), + (401, 2000, 'Awesome'); + diff --git a/server/database/src/main/resources/db/migration/error_report/V1.00.00__create_tables.sql b/server/database/src/main/resources/db/migration/error_report/V1.00.00__create_tables.sql new file mode 100644 index 0000000..f0bef3d --- /dev/null +++ b/server/database/src/main/resources/db/migration/error_report/V1.00.00__create_tables.sql @@ -0,0 +1,43 @@ +create table error_report_history +( + id serial primary key, + user_ip character varying(30) not null, + date_created timestamp not null default now() +); +alter table error_report_history owner to error_report_user; +comment on table error_report_history is + $$Table that stores timestamps by user IP address of when error reports were created. Used to do rate limiting.$$; +comment on column error_report_history.user_ip is 'IP address of a user that has submitted an error report'; +comment on column error_report_history.date_created is 'Timestamp when error report was created in DB'; + + +alter table error_report_history + add column system_id varchar(36); +alter table error_report_history + add column report_title varchar(125); +alter table error_report_history + add column game_version varchar(16); +alter table error_report_history + add column created_issue_link varchar(128); + +update error_report_history +set system_id = '---' || random(), + report_title = '---' || random(), + game_version = '2.0.20234', + created_issue_link = '----' || random(); + +alter table error_report_history + alter column system_id set not null; +alter table error_report_history + alter column report_title set not null; +alter table error_report_history + alter column game_version set not null; +alter table error_report_history + alter column created_issue_link set not null; + +-- the title and version pair should be unique, otherwise we are looking at a duplicate +alter table error_report_history + add constraint error_report_history_unique_title_version unique (report_title, game_version); +-- all links should be unique +alter table error_report_history + add constraint error_report_history_unique_link unique (created_issue_link); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.00__lobby.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.00__lobby.sql new file mode 100644 index 0000000..5879b74 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.00__lobby.sql @@ -0,0 +1,224 @@ +comment on database lobby_db is 'Database used by lobby'; + +create table bad_word +( + word character varying(40) not null primary key, + date_created timestamp without time zone not null default now() +); +alter table bad_word + owner to lobby_user; +comment on table bad_word is 'A table representing a blacklist of words, usernames may not contain these words.'; + + +create table banned_username +( + username character varying(40) not null primary key, + date_created timestamptz not null default now() +); +alter table banned_username + owner to lobby_user; +comment on table banned_username is 'A Table storing a username blacklilst.'; +comment on column banned_username.username is 'The blacklisted username'; + +create table user_role +( + id int primary key, + name character varying(16) not null unique +); + +comment on table user_role is 'Table storing the names of the different user (authentication) roles.'; +insert into user_role(id, name) +values (1, 'ADMIN'), -- user can add/remove admins and add/remove moderators, has boot/ban privileges + (2, 'MODERATOR'), -- user has boot/ban privileges + (3, 'PLAYER'), -- standard registered user + (4, 'ANONYMOUS'), -- users that are not registered, they do not have an entry in lobby_user table + (5, 'HOST'); -- AKA 'Lobby Watcher', special connection for hosts to send game updates to lobby + +create table lobby_user +( + id serial primary key, + username character varying(40) not null unique, + password character varying(60), + email character varying(40) not null check (email like '%@%' and email <> ''), + date_created timestamptz not null default current_timestamp, + last_login timestamptz, + user_role_id int not null references user_role (id), + bcrypt_password character(60) check (char_length(bcrypt_password) = 60) +); +alter table lobby_user + owner to lobby_user; +alter table lobby_user + add constraint lobby_user_pass_check check (password IS NOT NULL OR bcrypt_password IS NOT NULL); +comment on table lobby_user is 'Stores information about Triplea Lobby users.'; +comment on column lobby_user.username is 'Defines the in-game username.'; +comment on column lobby_user.password is + $$The legacy MD5Crypt hash of the password. The length of the hash must always be 34 chars. + Either password or bcrypt_password must be not null.$$; +comment on column lobby_user.email is + $$Email storage of every user. Large size to match the maximum email length. + More information here: https://stackoverflow.com/a/574698.$$; +comment on column lobby_user.bcrypt_password is + $$The BCrypt-Hashed password of the user, should be the same as the md5 password but in another form. + The length of the hash must always be 60 chars. Either password or bcrypt_password must be not null.$$; + + +create table access_log +( + access_time timestamptz not null default now(), + username varchar(40) not null, + ip inet not null, + system_id varchar(36) not null, + registered boolean not null +); + +alter table access_log + owner to lobby_user; + +comment on table access_log is + $$ Audit table recording access to the lobby. $$; + +comment on column access_log.registered is + $$ True if the user was registered when accessing the lobby; + otherwise false if the user was anonymous$$; + + +create table moderator_action_history +( + id serial primary key, + lobby_user_id int not null references lobby_user (id), + date_created timestamptz not null default current_timestamp, + action_name varchar(64) not null, + action_target varchar(40) not null +); +alter table moderator_action_history + owner to lobby_user; +comment on table moderator_action_history is 'Table storing an audit history of actions taken by moderators'; +comment on column moderator_action_history.lobby_user_id is 'FK to lobby_user table, this is the moderator that initiated an action.'; +comment on column moderator_action_history.date_created is 'Row creation timestamp, when the action was taken.'; +comment on column moderator_action_history.action_name is 'Specifier of what action the moderator took, eg: ban|mute'; +comment on column moderator_action_history.action_target is 'The target of the action, eg: banned player name, banned IP'; + + +create table error_report_history +( + id serial primary key, + user_ip character varying(30) not null, + date_created timestamp not null default now() +); +alter table error_report_history + owner to lobby_user; +comment on table error_report_history is + $$Table that stores timestamps by user IP address of when error reports were created. Used to do rate limiting.$$; +comment on column error_report_history.user_ip is 'IP address of a user that has submitted an error report'; +comment on column error_report_history.date_created is 'Timestamp when error report was created in DB'; + + +create table banned_user +( + id serial primary key, + public_id varchar(36) not null unique, + username varchar(40) not null, + system_id varchar(36) not null, + ip inet not null, + ban_expiry timestamptz not null, + date_created timestamptz not null default now() +); +alter table banned_user + owner to lobby_user; + +create index banned_user_ip on banned_user(ip, system_id); + +comment on table banned_user is + $$ Table that records player bans, when players join lobby we check their IP address and hashed mac + against this table. If there there is an IP or mac match, then the user is not allowed to join. $$; +comment on column banned_user.public_id is + $$ A value that publicly identifiers the ban. When a player is rejected from joining lobby we can + show them this ID value. If the player wants to dispute the ban, they can give us the public id + and we would be able to remove the ban. $$; +comment on column banned_user.username is + $$ Record of the name of the user at the time of ban, used for reference purposes. $$; + + +create table temp_password_request +( + id serial primary key, + lobby_user_id int not null references lobby_user (id), + temp_password varchar(60) not null, + date_created timestamptz not null default now(), + date_invalidated timestamptz +); +alter table temp_password_request + owner to lobby_user; +comment on table temp_password_request is + $$Table that stores temporary passwords issued to players. They are intended to be single use.$$; +comment on column temp_password_request.lobby_user_id is 'FK to lobby_user table.'; +comment on column temp_password_request.temp_password is 'Temp password value created for user.'; +comment on column temp_password_request.date_created is 'Timestamp of when the ban temporary password was created.'; +comment on column temp_password_request.date_invalidated is + $$Timestamp of when the temporary password is either used or marked invalid. + A temp password can be marked as invalid if multiple are issued.$$; + + +create table temp_password_request_history +( + id serial primary key, + inetaddress inet not null, + username varchar(40) not null, + date_created timestamptz not null default now() +); +alter table temp_password_request_history + owner to lobby_user; +comment on table temp_password_request_history is + $$Table that stores requests for temporary passwords for audit purposes. This will let us rate limit requests and + prevent a single player from spamming email to many userse.$$; +comment on column temp_password_request_history.inetaddress is 'IP of the address making the temp password request.'; +comment on column temp_password_request_history.username is 'The requested username for a temp password.'; +comment on column temp_password_request_history.date_created is 'Timestamp of when the temp password request is made'; +create index temp_password_request_history_inet on temp_password_request_history (inetaddress); + + +create table lobby_api_key +( + id serial primary key, + username varchar(40) not null, + lobby_user_id int references lobby_user (id), + user_role_id int not null references user_role (id), + player_chat_id varchar(36) not null unique, + key varchar(256) not null unique, + system_id varchar(36) not null, + ip inet not null, + date_created timestamptz not null default now() +); + +alter table lobby_api_key + owner to lobby_user; +comment on table lobby_api_key is + $$ Table that stores api keys of lobby-connected users along with identifiers. Note, old records + are periodically dropped from the table to keep the table length manageable. $$; +comment on column lobby_api_key.username is + $$ The name of the player at the time of login. For registered players, this will match their +registered name. $$; +comment on column lobby_api_key.system_id is + $$ Identifier that is generated on client system and is passed to the backend. Identifies the +users devices. $$; +comment on column lobby_api_key.player_chat_id is + $$ Identifier assigned to the player when they enter chat, used as a unique identifier +to reference a player that has joined the lobby. This value is public facing and is sent +to the front-end so that moderators can send the value back when requesting users to be banned. $$; +comment on column lobby_api_key.key is + $$ API key value granted to a user. Users are allowed to have multiple API-keys if they +login in multiple times. $$; + + +create table game_hosting_api_key +( + id serial primary key, + key character varying(256) not null unique, + ip inet not null, + date_created timestamptz not null default now() +); +alter table game_hosting_api_key + owner to lobby_user; +comment on table game_hosting_api_key is + $$ Table dedicated for storing "Lobby Watcher" api-keys. Game hosting connections create a dedicated + connection to the lobby with their own API key $$; diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.01__add_chat_history.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.01__add_chat_history.sql new file mode 100644 index 0000000..e7a016e --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.01__add_chat_history.sql @@ -0,0 +1,23 @@ +create table lobby_chat_history +( + id serial primary key, + date timestamp without time zone not null default now(), + username character varying(40) not null, + lobby_api_key_id int not null references lobby_api_key (id), + message character varying(240) not null +); + +comment on table lobby_chat_history is + $$Table recording history of all chat messages in the lobby$$; +comment on column lobby_chat_history.username is + $$De-normalized column representing the name of the user that + sent the chat message. It is denormalized for query convenience.$$; +comment on column lobby_chat_history.lobby_api_key_id is + $$Foreign key reference to the API-key used by the user when logging in. Useful to + know the users IP and system id information for cross-reference.$$; + +comment on column lobby_chat_history.message is + $$The contents of the chat message sent by the user$$; + +create index lobby_chat_date on lobby_chat_history (date); +create index lobby_chat_username on lobby_chat_history (username); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.02__game_and_player_history.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.02__game_and_player_history.sql new file mode 100644 index 0000000..bd1643d --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.02__game_and_player_history.sql @@ -0,0 +1,39 @@ +create table lobby_game +( + id serial primary key, + host_name character varying(40) not null, + game_id character varying(36) not null, + game_hosting_api_key_id int not null references game_hosting_api_key (id), + date_created timestamp without time zone not null default now() +); + +comment on table lobby_game is + $$Records games that have been posted to the lobby$$; +comment on column lobby_game.host_name is + $$Name of the game host, can be a bot or a player name$$; +comment on column lobby_game.game_id is + $$ID of the game as assigned by the lobby. 'game_id' is a publicly known field and is sent to players. + A single game can have multiple game-ids if it is disconnected and reconnects.$$; + +create index lobby_game_host_name_game_id on lobby_game (host_name, game_id); + + +create table game_chat_history +( + id serial primary key, + lobby_game_id int not null references lobby_game (id), + date timestamp without time zone not null default now(), + username character varying(40) not null, + message character varying(240) not null +); + +comment on table game_chat_history is + $$Table recording history of chat messages in games connected to the lobby. + Of note, players can connect directly to lobby games and may not necessarily have an API key.$$; +comment on column game_chat_history.username is + $$The name of the player sending a chat message$$; +comment on column game_chat_history.message is + $$Contents of the chat message sent$$; + +create index game_chat_history_username on game_chat_history (username); +create index game_chat_history_date on game_chat_history (date); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.03__change_game_history_index.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.03__change_game_history_index.sql new file mode 100644 index 0000000..1172700 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.03__change_game_history_index.sql @@ -0,0 +1,11 @@ +drop index lobby_game_host_name_game_id; +create index lobby_game_game_id on lobby_game (game_id); + +alter table lobby_chat_history + alter column date type timestamptz; +alter table game_chat_history + alter column date type timestamptz; +alter table lobby_game + alter column date_created type timestamptz; +alter table bad_word + alter column date_created type timestamptz; diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.04__add_lobby_user_fk_to_access_log.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.04__add_lobby_user_fk_to_access_log.sql new file mode 100644 index 0000000..331dbb5 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.04__add_lobby_user_fk_to_access_log.sql @@ -0,0 +1,3 @@ +alter table access_log drop column registered; +alter table access_log add column lobby_user_id int references lobby_user(id); +alter table lobby_user drop column last_login; diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.05__drop_legacy_password.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.05__drop_legacy_password.sql new file mode 100644 index 0000000..5b7c872 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.05__drop_legacy_password.sql @@ -0,0 +1,2 @@ +alter table lobby_user drop column password; +alter table lobby_user alter column bcrypt_password set not null; diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.00.06__error_report_issue_and_title_column.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.00.06__error_report_issue_and_title_column.sql new file mode 100644 index 0000000..d473914 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.00.06__error_report_issue_and_title_column.sql @@ -0,0 +1,30 @@ +alter table error_report_history + add column system_id varchar(36); +alter table error_report_history + add column report_title varchar(125); +alter table error_report_history + add column game_version varchar(16); +alter table error_report_history + add column created_issue_link varchar(128); + +update error_report_history +set system_id = '---' || random(), + report_title = '---' || random(), + game_version = '2.0.20234', + created_issue_link = '----' || random(); + +alter table error_report_history + alter column system_id set not null; +alter table error_report_history + alter column report_title set not null; +alter table error_report_history + alter column game_version set not null; +alter table error_report_history + alter column created_issue_link set not null; + +-- the title and version pair should be unique, otherwise we are looking at a duplicate +alter table error_report_history + add constraint error_report_history_unique_title_version unique (report_title, game_version); +-- all links should be unique +alter table error_report_history + add constraint error_report_history_unique_link unique (created_issue_link); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.01.00__fix_username_ban_casing.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.01.00__fix_username_ban_casing.sql new file mode 100644 index 0000000..1b33b89 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.01.00__fix_username_ban_casing.sql @@ -0,0 +1,11 @@ +-- delete any extra name bans that are duplicates +delete from banned_username bu +where exists ( + select 1 + from banned_username t2 + where bu.date_created < t2.date_created + and bu.username ilike t2.username +); + +-- upper case existing entries +update banned_username set username = upper(username); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.01.01__maps_index_tables.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.01.01__maps_index_tables.sql new file mode 100644 index 0000000..a2defd3 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.01.01__maps_index_tables.sql @@ -0,0 +1,33 @@ +create table map_index +( + id serial primary key, + map_name varchar(256) not null, + last_commit_date timestamptz not null check (last_commit_date < now()), + repo_url varchar(256) not null unique check (repo_url like 'http%'), + preview_image_url varchar(256) not null check (repo_url like 'http%'), + description varchar(3000) not null, + download_size_bytes integer not null, + download_url varchar(256) not null unique check (download_url like 'http%'), + date_created timestamptz not null default now(), + date_updated timestamptz not null default now() +); + +create table tag_type +( + id int primary key, + name varchar(64) not null unique, + type varchar(32) not null, + -- disallow display_order values over 1000, we do not expect nearly that many unique tag types + display_order int not null unique check (display_order >= 0 and display_order < 1000) +); + +create table map_tag_values +( + id serial primary key, + map_id integer references map_index (id), + tag_type_id integer references tag_type (id), + tag_value varchar(128) not null +); + +alter table map_tag_values + add constraint map_id__tag_id__is__unique unique (map_id, tag_type_id); diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.01.02__map_tags.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.01.02__map_tags.sql new file mode 100644 index 0000000..f7ab86e --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.01.02__map_tags.sql @@ -0,0 +1,34 @@ +drop table map_tag_values; +drop table tag_type; + +create table map_tag +( + id serial primary key, + name varchar(64) not null unique, + display_order int not null unique check (display_order >=0 and display_order <= 1000) +); +comment on table map_tag is 'Defines the set of map tags.'; + +create table map_tag_allowed_value +( + id serial primary key, + map_tag_id int not null references map_tag(id), + value varchar(64) not null +); +alter table map_tag_allowed_value + add constraint map_tag_values_unique unique (map_tag_id, value); + +comment on table map_tag_allowed_value is 'Defines the set of allowed values per tag.'; + +create table map_tag_value +( + id serial primary key, + map_tag_id int not null references map_tag(id), + map_index_id int not null references map_index(id), + map_tag_allowed_value_id int not null references map_tag_allowed_value(id) +); + +alter table map_tag_value + add constraint map_tag_value_unique unique (map_tag_id, map_index_id); + +comment on table map_tag_value is 'Join table relating maps to tags'; diff --git a/server/database/src/main/resources/db/migration/lobby_db/V2.02.00__lobby_message.sql b/server/database/src/main/resources/db/migration/lobby_db/V2.02.00__lobby_message.sql new file mode 100644 index 0000000..a2b7e18 --- /dev/null +++ b/server/database/src/main/resources/db/migration/lobby_db/V2.02.00__lobby_message.sql @@ -0,0 +1,9 @@ +create table lobby_message +( + message varchar(256) not null +); +comment on table lobby_message is 'Stores the message that we display to users on login'; + +insert into lobby_message(message) +values ('Welcome to the TripleA lobby!\n' || + 'Please no politics, stay respectful, be welcoming, have fun.'); diff --git a/server/database/start_docker_db b/server/database/start_docker_db new file mode 100755 index 0000000..a2d5b0c --- /dev/null +++ b/server/database/start_docker_db @@ -0,0 +1,51 @@ +#!/bin/bash + +set -eu + +bold="\e[1m" +bold_green="${bold}\e[32m" +normal="\e[0m" + +function main() { + echo -e "[${bold_green}STARTING DATABASE${normal}]" + + runDocker + waitForDatabaseToStart + echo -e "[${bold_green}DONE${normal}]" +} + +function runDocker() { + echo "Stopping database.." + docker stop database || echo "[OK] Database not running.." + docker run \ + --rm -d \ + --name="database" \ + -e "POSTGRES_PASSWORD=postgres" \ + -p "5432:5432" \ + -v "$(pwd)/$(dirname "$0")/sql/init/:/docker-entrypoint-initdb.d/" \ + "postgres:10" +} + +function waitForDatabaseToStart() { + echo -n "Waiting for Database start.." + tryAttempt=0 + export PGPASSWORD=postgres + while ! ( + echo 'select 1' \ + | psql -h localhost -U postgres 2> /dev/null \ + | grep -q '1 row'); do + + sleep 0.2 + echo -n . + + tryAttempt=$((tryAttempt+1)) + # timeout after 10s + if [ "$tryAttempt" -gt 50 ]; then + echo "Aborting DB startup (timed out)" + exit 1 + fi + done + echo "" +} + +main diff --git a/server/dropwizard-server/build.gradle b/server/dropwizard-server/build.gradle new file mode 100644 index 0000000..2506dd8 --- /dev/null +++ b/server/dropwizard-server/build.gradle @@ -0,0 +1,75 @@ +plugins { + id "java" + id "application" + id "com.github.johnrengelman.shadow" version "7.1.2" +} + +archivesBaseName = "$group-$name" +mainClassName = "org.triplea.spitfire.server.SpitfireServerApplication" +ext { + releasesDir = file("$buildDir/releases") +} + +jar { + manifest { + attributes "Main-Class": mainClassName + } +} + +task portableInstaller(type: Zip, group: "release", dependsOn: shadowJar) { + from file("configuration.yml") + + from(shadowJar.outputs) { + into "bin" + } +} + +task release(group: "release", dependsOn: portableInstaller) { + doLast { + publishArtifacts(portableInstaller.outputs.files) + } +} + +shadowJar { + archiveClassifier.set "" + // mergeServiceFiles is needed by dropwizard + // Without this configuration parsing breaks and is unable to find connector type "http" for + // the following YAML snippet: server: {applicationConnectors: [{type: http, port: 8080}] + mergeServiceFiles() +} + +configurations { + testImplementation { + // database-rider brings in slf4j-simple as a transitive dependency + // DropWizard has logback baked in and cannot have multiple slf4j bindings. + exclude group: "org.slf4j", module: "slf4j-simple" + } +} + +dependencies { + implementation "com.liveperson:dropwizard-websockets:$dropwizardWebsocketsVersion" + implementation "io.dropwizard:dropwizard-auth:$dropwizardVersion" + implementation "io.dropwizard:dropwizard-core:$dropwizardVersion" + implementation "io.dropwizard:dropwizard-jdbi3:$dropwizardVersion" +// implementation project(":game-app:domain-data") +// implementation project(":http-clients:github-client") +// implementation project(":http-clients:lobby-client") + + implementation("triplea:lobby-client:2.6.14756") + +// implementation project(":lib:feign-common") +// implementation project(":lib:java-extras") +// implementation project(":lib:websocket-client") +// implementation project(":lib:websocket-server") + implementation project(":server:server-lib") + implementation project(":server:lobby-module") + testImplementation "com.github.database-rider:rider-junit5:$databaseRiderVersion" + testImplementation "io.dropwizard:dropwizard-testing:$dropwizardVersion" + testImplementation "io.github.openfeign:feign-core:$feignCoreVersion" + testImplementation "io.github.openfeign:feign-gson:$feignGsonVersion" + testImplementation "org.awaitility:awaitility:$awaitilityVersion" + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion" +// testImplementation project(":lib:test-common") + testImplementation "org.java-websocket:Java-WebSocket:$javaWebSocketVersion" + runtimeOnly "org.postgresql:postgresql:$postgresqlVersion" +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/ResponseStatus.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/ResponseStatus.java new file mode 100644 index 0000000..be2229c --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/ResponseStatus.java @@ -0,0 +1,31 @@ +package org.triplea.spitfire.server; + +import javax.annotation.Nonnull; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status.Family; +import lombok.Builder; +import lombok.Getter; + +/** + * Creates a custom http Status Code, mainly used to be able to create a custom reason phrase. This + * is useful if we cannot transmit an http body and need to pass information through the status + * reason. + */ +@Builder +public class ResponseStatus implements Response.StatusType { + @Getter(onMethod_ = @Override) + @Nonnull + private final String reasonPhrase; + + @Nonnull private final Integer statusCode; + + @Override + public Family getFamily() { + return Family.familyOf(getStatusCode()); + } + + @Override + public int getStatusCode() { + return statusCode; + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerApplication.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerApplication.java new file mode 100644 index 0000000..309dbed --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerApplication.java @@ -0,0 +1,167 @@ +package org.triplea.spitfire.server; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.Application; +import io.dropwizard.jdbi3.JdbiFactory; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import java.time.Duration; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.LobbyModuleRowMappers; +import org.triplea.dropwizard.common.AuthenticationConfiguration; +import org.triplea.dropwizard.common.IllegalArgumentMapper; +import org.triplea.dropwizard.common.JdbiLogging; +import org.triplea.dropwizard.common.ServerConfiguration; +import org.triplea.dropwizard.common.ServerConfiguration.WebsocketConfig; +import org.triplea.http.client.web.socket.WebsocketPaths; +import org.triplea.modules.chat.ChatMessagingService; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.modules.latest.version.LatestVersionModule; +import org.triplea.spitfire.server.access.authentication.ApiKeyAuthenticator; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; +import org.triplea.spitfire.server.access.authorization.BannedPlayerFilter; +import org.triplea.spitfire.server.access.authorization.RoleAuthorizer; +import org.triplea.spitfire.server.controllers.latest.version.LatestVersionController; +import org.triplea.spitfire.server.controllers.lobby.GameHostingController; +import org.triplea.spitfire.server.controllers.lobby.GameListingController; +import org.triplea.spitfire.server.controllers.lobby.LobbyWatcherController; +import org.triplea.spitfire.server.controllers.lobby.PlayersInGameController; +import org.triplea.spitfire.server.controllers.lobby.moderation.AccessLogController; +import org.triplea.spitfire.server.controllers.lobby.moderation.BadWordsController; +import org.triplea.spitfire.server.controllers.lobby.moderation.DisconnectUserController; +import org.triplea.spitfire.server.controllers.lobby.moderation.GameChatHistoryController; +import org.triplea.spitfire.server.controllers.lobby.moderation.ModeratorAuditHistoryController; +import org.triplea.spitfire.server.controllers.lobby.moderation.ModeratorsController; +import org.triplea.spitfire.server.controllers.lobby.moderation.MuteUserController; +import org.triplea.spitfire.server.controllers.lobby.moderation.RemoteActionsController; +import org.triplea.spitfire.server.controllers.lobby.moderation.UserBanController; +import org.triplea.spitfire.server.controllers.lobby.moderation.UsernameBanController; +import org.triplea.spitfire.server.controllers.user.account.CreateAccountController; +import org.triplea.spitfire.server.controllers.user.account.ForgotPasswordController; +import org.triplea.spitfire.server.controllers.user.account.LoginController; +import org.triplea.spitfire.server.controllers.user.account.PlayerInfoController; +import org.triplea.spitfire.server.controllers.user.account.UpdateAccountController; +import org.triplea.web.socket.GameConnectionWebSocket; +import org.triplea.web.socket.GenericWebSocket; +import org.triplea.web.socket.PlayerConnectionWebSocket; +import org.triplea.web.socket.SessionBannedCheck; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** + * Main entry-point for launching drop wizard HTTP server. This class is responsible for configuring + * any Jersey plugins, registering resources (controllers) and injecting those resources with + * configuration properties from 'AppConfig'. + */ +@Slf4j +public class SpitfireServerApplication extends Application { + + private static final String[] DEFAULT_ARGS = new String[] {"server", "configuration.yml"}; + + private ServerConfiguration serverConfiguration; + + /** + * Main entry-point method, launches the drop-wizard http server. If no args are passed then will + * use default values suitable for local development. + */ + public static void main(final String[] args) throws Exception { + final SpitfireServerApplication application = new SpitfireServerApplication(); + // if no args are provided then we will use default values. + application.run(args.length == 0 ? DEFAULT_ARGS : args); + } + + @Override + public void initialize(final Bootstrap bootstrap) { + serverConfiguration = + ServerConfiguration.build( + bootstrap, + new WebsocketConfig(GameConnectionWebSocket.class, WebsocketPaths.GAME_CONNECTIONS), + new WebsocketConfig( + PlayerConnectionWebSocket.class, WebsocketPaths.PLAYER_CONNECTIONS)) + .enableEnvironmentVariablesInConfig() + .enableBetterJdbiExceptions(); + } + + @Override + public void run(final SpitfireServerConfig configuration, final Environment environment) { + final Jdbi jdbi = + new JdbiFactory() + .build(environment, configuration.getDatabase(), "postgresql-connection-pool"); + + LobbyModuleRowMappers.rowMappers().forEach(jdbi::registerRowMapper); + + if (configuration.isLogSqlStatements()) { + JdbiLogging.registerSqlLogger(jdbi); + } + + final LatestVersionModule latestVersionModule = new LatestVersionModule(); + if (configuration.isLatestVersionFetcherEnabled()) { + log.info("Latest Engine Version Fetching running every 30 minutes"); + environment + .lifecycle() + .manage( + latestVersionModule.buildRefreshSchedule( + configuration, + LatestVersionModule.RefreshConfiguration.builder() + .delay(Duration.ofSeconds(10L)) + .period(Duration.ofMinutes(30L)) + .build())); + } else { + log.info("Latest Engine Version Fetching is disabled"); + } + + serverConfiguration.registerRequestFilter( + environment, BannedPlayerFilter.newBannedPlayerFilter(jdbi)); + + final MetricRegistry metrics = new MetricRegistry(); + AuthenticationConfiguration.enableAuthentication( + environment, + metrics, + ApiKeyAuthenticator.build(jdbi), + new RoleAuthorizer(), + AuthenticatedUser.class); + + serverConfiguration.registerExceptionMappers(environment, List.of(new IllegalArgumentMapper())); + + final var sessionIsBannedCheck = SessionBannedCheck.build(jdbi); + final var gameConnectionMessagingBus = new WebSocketMessagingBus(); + + GenericWebSocket.init( + GameConnectionWebSocket.class, gameConnectionMessagingBus, sessionIsBannedCheck); + + final var playerConnectionMessagingBus = new WebSocketMessagingBus(); + GenericWebSocket.init( + PlayerConnectionWebSocket.class, playerConnectionMessagingBus, sessionIsBannedCheck); + + final var chatters = Chatters.build(); + ChatMessagingService.build(chatters, jdbi).configure(playerConnectionMessagingBus); + + final GameListing gameListing = GameListing.build(jdbi, playerConnectionMessagingBus); + List.of( + // lobby module controllers + AccessLogController.build(jdbi), + BadWordsController.build(jdbi), + CreateAccountController.build(jdbi), + DisconnectUserController.build(jdbi, chatters, playerConnectionMessagingBus), + ForgotPasswordController.build(configuration, jdbi), + GameChatHistoryController.build(jdbi), + GameHostingController.build(jdbi), + GameListingController.build(gameListing), + LobbyWatcherController.build(configuration, jdbi, gameListing), + LoginController.build(jdbi, chatters), + UsernameBanController.build(jdbi), + UserBanController.build( + jdbi, chatters, playerConnectionMessagingBus, gameConnectionMessagingBus), + ModeratorAuditHistoryController.build(jdbi), + ModeratorsController.build(jdbi), + MuteUserController.build(chatters), + PlayerInfoController.build(jdbi, chatters, gameListing), + PlayersInGameController.build(gameListing), + RemoteActionsController.build(jdbi, gameConnectionMessagingBus), + UpdateAccountController.build(jdbi), + LatestVersionController.build(latestVersionModule)) + .forEach(controller -> environment.jersey().register(controller)); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerConfig.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerConfig.java new file mode 100644 index 0000000..492dc83 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/SpitfireServerConfig.java @@ -0,0 +1,77 @@ +package org.triplea.spitfire.server; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; +import java.net.URI; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.triplea.http.client.github.GithubApiClient; +import org.triplea.modules.LobbyModuleConfig; +import org.triplea.modules.latest.version.LatestVersionModuleConfig; + +/** + * This configuration class represents the configuration values in the server YML configuration. An + * instance of this class is created by DropWizard on launch and then is passed to the application + * class. Values can be injected into the application by using environment variables in the server + * YML configuration file. + */ +public class SpitfireServerConfig extends Configuration + implements LobbyModuleConfig, LatestVersionModuleConfig { + + /** + * Flag that indicates if we are running in production. This can be used to verify we do not have + * any magic stubbing and to do any additional configuration checks to be really sure production + * is well configured. Because we vary configuration values between prod and test, there can be + * prod-only cases where we perhaps have something misconfigured, hence the risk we are trying to + * defend against. + */ + @Getter(onMethod_ = {@JsonProperty, @Override}) + @Setter(onMethod_ = {@JsonProperty}) + private boolean gameHostConnectivityCheckEnabled; + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private boolean logSqlStatements; + + @Valid @NotNull @JsonProperty @Getter + private final DataSourceFactory database = new DataSourceFactory(); + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private String githubWebServiceUrl; + + /** Webservice token, should be an API token for the TripleA builder bot account. */ + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private String githubApiToken; + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private boolean errorReportToGithubEnabled; + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private String githubGameOrg; + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private String githubGameRepo; + + @Getter(onMethod_ = {@JsonProperty}) + @Setter(onMethod_ = {@JsonProperty}) + private boolean latestVersionFetcherEnabled; + + @Override + public GithubApiClient createGamesRepoGithubApiClient() { + return GithubApiClient.builder() + .stubbingModeEnabled(!errorReportToGithubEnabled) + .authToken(githubApiToken) + .uri(URI.create(githubWebServiceUrl)) + .org(githubGameOrg) + .repo(githubGameRepo) + .build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticator.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticator.java new file mode 100644 index 0000000..cd37fc2 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticator.java @@ -0,0 +1,53 @@ +package org.triplea.spitfire.server.access.authentication; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Authenticator; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.GameHostingApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; + +/** + * Verifies a 'bearer' token API key is valid. This means checking if the key is in database, if so + * we return an {@code AuthenticatedUser} otherwise optional. Anonymous users will have a null + * user-id and role of 'ANONYMOUS'. + */ +@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor_ = @VisibleForTesting) +public class ApiKeyAuthenticator implements Authenticator { + + private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + private final GameHostingApiKeyDaoWrapper gameHostingApiKeyDaoWrapper; + + public static ApiKeyAuthenticator build(final Jdbi jdbi) { + return new ApiKeyAuthenticator( + PlayerApiKeyDaoWrapper.build(jdbi), GameHostingApiKeyDaoWrapper.build(jdbi)); + } + + @Override + public Optional authenticate(final String apiKey) { + final ApiKey key = ApiKey.of(apiKey); + return apiKeyDaoWrapper + .lookupByApiKey(key) + .map( + userData -> + AuthenticatedUser.builder() + .userId(userData.getUserId()) + .name(userData.getUsername()) + .userRole(userData.getUserRole()) + .apiKey(key) + .build()) + .or( + () -> + gameHostingApiKeyDaoWrapper.isKeyValid(key) + ? Optional.of( + AuthenticatedUser.builder() // + .userRole(UserRole.HOST) + .apiKey(key) + .build()) + : Optional.empty()); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUser.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUser.java new file mode 100644 index 0000000..c44bac3 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUser.java @@ -0,0 +1,46 @@ +package org.triplea.spitfire.server.access.authentication; + +import com.google.common.base.Preconditions; +import java.security.Principal; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; + +/** + * AuthenticatedUser is anyone that has successfully logged in to the lobby, whether via anonymous + * account or registered account. Because we must extend Principle, we have a no-op 'getName' method + * that throws an {@code UnsupportedOperationException} if accessed. + */ +@Builder +@EqualsAndHashCode +public class AuthenticatedUser implements Principal { + @Getter @Nullable private final Integer userId; + @Getter @Nonnull private final String userRole; + @Getter @Nonnull private final ApiKey apiKey; + @Nullable private final String name; + + @Nullable + @Override + public String getName() { + Preconditions.checkState( + (name == null) == userRole.equals(UserRole.HOST), + "All user roles will have a name except the 'HOST' role (lobby watcher)"); + return name; + } + + public int getUserIdOrThrow() { + Preconditions.checkState( + userId != null, "This method is called when we expect user id to not be null"); + Preconditions.checkState( + !userRole.equals(UserRole.ANONYMOUS), + "Integrity check, anonymous user role implies null user id, userId = " + userId); + Preconditions.checkState( + !userRole.equals(UserRole.HOST), + "Integrity check, host user role implies null user id, userId = " + userId); + return userId; + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilter.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilter.java new file mode 100644 index 0000000..29592fa --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilter.java @@ -0,0 +1,75 @@ +package org.triplea.spitfire.server.access.authorization; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import java.time.Clock; +import java.time.Duration; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.Provider; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.eclipse.jetty.http.HttpStatus; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.ban.BanLookupRecord; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.dropwizard.common.IpAddressExtractor; +import org.triplea.http.client.LobbyHttpClientConfig; +import org.triplea.http.client.lobby.moderator.BanDurationFormatter; +import org.triplea.spitfire.server.ResponseStatus; + +@Provider +@PreMatching +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor_ = @VisibleForTesting) +public class BannedPlayerFilter implements ContainerRequestFilter { + + private final UserBanDao userBanDao; + private final Clock clock; + + @Context private HttpServletRequest request; + + public static BannedPlayerFilter newBannedPlayerFilter(final Jdbi jdbi) { + return new BannedPlayerFilter(jdbi.onDemand(UserBanDao.class), Clock.systemUTC()); + } + + @Override + public void filter(final ContainerRequestContext requestContext) { + if (Strings.emptyToNull(request.getHeader(LobbyHttpClientConfig.SYSTEM_ID_HEADER)) == null) { + // missing system id header, abort the request + requestContext.abortWith( + Response.status(Status.UNAUTHORIZED).entity("Invalid request").build()); + + } else { + // check if user is banned, if so abort the request + userBanDao + .lookupBan( + IpAddressExtractor.extractIpAddress(request), + request.getHeader(LobbyHttpClientConfig.SYSTEM_ID_HEADER)) + .map(this::formatBanMessage) + .ifPresent( + banMessage -> + requestContext.abortWith( + Response.status( + ResponseStatus.builder() + .statusCode(HttpStatus.UNAUTHORIZED_401) + .reasonPhrase(banMessage) + .build()) + .build())); + } + } + + private String formatBanMessage(final BanLookupRecord banLookupRecord) { + final long banMinutes = + Duration.between(clock.instant(), banLookupRecord.getBanExpiry()).toMinutes(); + final String banDuration = BanDurationFormatter.formatBanMinutes(banMinutes); + + return String.format("Banned %s, Ban-ID: %s", banDuration, banLookupRecord.getPublicBanId()); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizer.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizer.java new file mode 100644 index 0000000..b2450b3 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizer.java @@ -0,0 +1,47 @@ +package org.triplea.spitfire.server.access.authorization; + +import io.dropwizard.auth.Authorizer; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** + * Performs authorization. Authorization happens after authentication and answers the question of: + * given an authenticated user, do they have permissions for a given role? + */ +public class RoleAuthorizer implements Authorizer { + + /** Verifies a user is authorized to assume a requestedRole.
*/ + @Override + public boolean authorize(final AuthenticatedUser user, final String requestedRole) { + switch (user.getUserRole()) { + case UserRole.ADMIN: + return adminAuthorizedFor(requestedRole); + case UserRole.MODERATOR: + return moderatorAuthorizedFor(requestedRole); + case UserRole.PLAYER: + return playerAuthorizedFor(requestedRole); + case UserRole.ANONYMOUS: + return anonymousAuthorizedFor(requestedRole); + case UserRole.HOST: + return requestedRole.equals(UserRole.HOST); + default: + throw new AssertionError("Unrecognized user role: " + user.getUserRole()); + } + } + + private static boolean adminAuthorizedFor(final String requestedRole) { + return requestedRole.equals(UserRole.ADMIN) || moderatorAuthorizedFor(requestedRole); + } + + private static boolean moderatorAuthorizedFor(final String requestedRole) { + return requestedRole.equals(UserRole.MODERATOR) || playerAuthorizedFor(requestedRole); + } + + private static boolean playerAuthorizedFor(final String requestedRole) { + return requestedRole.equals(UserRole.PLAYER) || anonymousAuthorizedFor(requestedRole); + } + + private static boolean anonymousAuthorizedFor(final String requestedRole) { + return requestedRole.equals(UserRole.ANONYMOUS); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/latest/version/LatestVersionController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/latest/version/LatestVersionController.java new file mode 100644 index 0000000..ee6c6d2 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/latest/version/LatestVersionController.java @@ -0,0 +1,36 @@ +package org.triplea.spitfire.server.controllers.latest.version; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.triplea.http.client.latest.version.LatestVersionClient; +import org.triplea.http.client.latest.version.LatestVersionResponse; +import org.triplea.modules.latest.version.LatestVersionModule; +import org.triplea.spitfire.server.HttpController; + +@Builder +public class LatestVersionController extends HttpController { + private final LatestVersionModule latestVersionModule; + + public static LatestVersionController build(final LatestVersionModule latestVersionModule) { + return new LatestVersionController(latestVersionModule); + } + + @GET + @Path(LatestVersionClient.LATEST_VERSION_PATH) + public Response getLatestEngineVersion() { + return latestVersionModule + .getLatestVersion() + .map( + latest -> + Response.ok( + LatestVersionResponse.builder() // + .latestEngineVersion(latest) + .releaseNotesUrl("https://triplea-game.org/release_notes/") + .downloadUrl("https://triplea-game.org/download/") + .build()) + .build()) + .orElseGet(() -> Response.status(503, "Unable to fetch latest version").build()); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameHostingController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameHostingController.java new file mode 100644 index 0000000..1fc055e --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameHostingController.java @@ -0,0 +1,50 @@ +package org.triplea.spitfire.server.controllers.lobby; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.GameHostingApiKeyDaoWrapper; +import org.triplea.domain.data.ApiKey; +import org.triplea.dropwizard.common.IpAddressExtractor; +import org.triplea.http.client.lobby.game.hosting.request.GameHostingClient; +import org.triplea.http.client.lobby.game.hosting.request.GameHostingResponse; +import org.triplea.spitfire.server.HttpController; + +/** + * Provides an endpoint where an independent connection can be established, provides an API key to + * unauthenticated users that can then be used to post a game. Banning rules are verified to ensure + * banned users cannot post games. + */ +@Builder +public class GameHostingController extends HttpController { + + @Nonnull private final Function apiKeySupplier; + + public static GameHostingController build(final Jdbi jdbi) { + final var gameHostingApiKeyDaoWrapper = GameHostingApiKeyDaoWrapper.build(jdbi); + return GameHostingController.builder() // + .apiKeySupplier(gameHostingApiKeyDaoWrapper::newGameHostKey) + .build(); + } + + @POST + @Path(GameHostingClient.GAME_HOSTING_REQUEST_PATH) + public GameHostingResponse hostingRequest(@Context final HttpServletRequest request) { + String remoteIp = IpAddressExtractor.extractIpAddress(request); + try { + return GameHostingResponse.builder() + .apiKey(apiKeySupplier.apply(InetAddress.getByName(remoteIp)).getValue()) + .publicVisibleIp(remoteIp) + .build(); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException("Invalid IP address in request: " + remoteIp, e); + } + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameListingController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameListingController.java new file mode 100644 index 0000000..b41e203 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/GameListingController.java @@ -0,0 +1,53 @@ +package org.triplea.spitfire.server.controllers.lobby; + +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Auth; +import java.util.Collection; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.game.lobby.watcher.GameListingClient; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** Controller with endpoints for posting, getting and removing games. */ +@Builder +@AllArgsConstructor( + access = AccessLevel.PACKAGE, + onConstructor_ = {@VisibleForTesting}) +@RolesAllowed(UserRole.HOST) +public class GameListingController extends HttpController { + + private final GameListing gameListing; + + public static GameListingController build(final GameListing gameListing) { + return GameListingController.builder() // + .gameListing(gameListing) + .build(); + } + + /** Returns a listing of the current games. */ + @GET + @Path(GameListingClient.FETCH_GAMES_PATH) + @RolesAllowed(UserRole.ANONYMOUS) + public Collection fetchGames() { + return gameListing.getGames(); + } + + /** Moderator action to remove a game. */ + @POST + @Path(GameListingClient.BOOT_GAME_PATH) + @RolesAllowed(UserRole.MODERATOR) + public Response bootGame(@Auth final AuthenticatedUser authenticatedUser, final String gameId) { + gameListing.bootGame(authenticatedUser.getUserIdOrThrow(), gameId); + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/LobbyWatcherController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/LobbyWatcherController.java new file mode 100644 index 0000000..567345a --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/LobbyWatcherController.java @@ -0,0 +1,189 @@ +package org.triplea.spitfire.server.controllers.lobby; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpStatus; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.LobbyConstants; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatMessageUpload; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingResponse; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyWatcherClient; +import org.triplea.http.client.lobby.game.lobby.watcher.PlayerJoinedNotification; +import org.triplea.http.client.lobby.game.lobby.watcher.PlayerLeftNotification; +import org.triplea.http.client.lobby.game.lobby.watcher.UpdateGameRequest; +import org.triplea.modules.LobbyModuleConfig; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.modules.game.lobby.watcher.ChatUploadModule; +import org.triplea.modules.game.lobby.watcher.GamePostingModule; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** Controller with endpoints for posting, getting and removing games. */ +@Builder +@AllArgsConstructor( + access = AccessLevel.PACKAGE, + onConstructor_ = {@VisibleForTesting}) +@RolesAllowed(UserRole.HOST) +@Slf4j +public class LobbyWatcherController extends HttpController { + @VisibleForTesting + public static final String TEST_ONLY_GAME_POSTING_PATH = "/test-only/lobby/post-game"; + + @Nonnull private final Boolean gameHostConnectivityCheckEnabled; + @Nonnull private final GameListing gameListing; + @Nonnull private final ChatUploadModule chatUploadModule; + @Nonnull private final GamePostingModule gamePostingModule; + + public static LobbyWatcherController build( + final LobbyModuleConfig lobbyModuleConfig, final Jdbi jdbi, final GameListing gameListing) { + return LobbyWatcherController.builder() + .gameHostConnectivityCheckEnabled(lobbyModuleConfig.isGameHostConnectivityCheckEnabled()) + .gameListing(gameListing) + .chatUploadModule(ChatUploadModule.build(jdbi, gameListing)) + .gamePostingModule(GamePostingModule.build(gameListing)) + .build(); + } + + /** + * Adds a game to the lobby listing. Responds with the gameId assigned to the new game. If we see + * duplicate posts, the same gameId will be returned. + */ + @POST + @Path(LobbyWatcherClient.POST_GAME_PATH) + public GamePostingResponse postGame( + @Auth final AuthenticatedUser authenticatedUser, + final GamePostingRequest gamePostingRequest) { + Preconditions.checkArgument(gamePostingRequest != null); + Preconditions.checkArgument(gamePostingRequest.getLobbyGame() != null); + + return gamePostingModule.postGame(authenticatedUser.getApiKey(), gamePostingRequest); + } + + /** + * A endpoint available for non-prod-only that allows for integration tests to post games without + * actually hosting a game themselves (bypasses the reverse connectivity check). + */ + @POST + @Path(TEST_ONLY_GAME_POSTING_PATH) + public Response postGameTestOnly( + @Auth final AuthenticatedUser authenticatedUser, + final GamePostingRequest gamePostingRequest) { + Preconditions.checkArgument(gamePostingRequest != null); + Preconditions.checkArgument(gamePostingRequest.getLobbyGame() != null); + + return gameHostConnectivityCheckEnabled + ? Response.status(HttpStatus.NOT_FOUND_404).build() + : Response.ok() + .entity( + GamePostingResponse.builder() + .connectivityCheckSucceeded(true) + .gameId(gameListing.postGame(authenticatedUser.getApiKey(), gamePostingRequest)) + .build()) + .build(); + } + + /** Explicit remove of a game from the lobby. */ + @POST + @Path(LobbyWatcherClient.REMOVE_GAME_PATH) + public Response removeGame(@Auth final AuthenticatedUser authenticatedUser, final String gameId) { + gameListing.removeGame(authenticatedUser.getApiKey(), gameId); + return Response.ok().build(); + } + + /** + * "Alive" endpoint to periodically invoked after a game has been posted to indicate the client is + * still hosting and is alive. If the endpoint is not invoked within a cutoff time then the game + * with the corresponding gameId will be unlisted. The return value indicates if the game has been + * kept alive, or false indicates the game was already removed and the client should re-post. + */ + @POST + @Path(LobbyWatcherClient.KEEP_ALIVE_PATH) + public boolean keepAlive(@Auth final AuthenticatedUser authenticatedUser, final String gameId) { + return gameListing.keepAlive(authenticatedUser.getApiKey(), gameId); + } + + /** Replaces an existing game with new game data details. */ + @POST + @Path(LobbyWatcherClient.UPDATE_GAME_PATH) + public Response updateGame( + @Auth final AuthenticatedUser authenticatedUser, final UpdateGameRequest updateGameRequest) { + gameListing.updateGame( + authenticatedUser.getApiKey(), + updateGameRequest.getGameId(), + updateGameRequest.getGameData()); + return Response.ok().build(); + } + + /** Endpoint used to consume and persist chat messages to database. */ + @POST + @Path(LobbyWatcherClient.UPLOAD_CHAT_PATH) + @RolesAllowed(UserRole.HOST) + public Response uploadChatMessage( + @Context final HttpServletRequest request, final ChatMessageUpload chatMessageUpload) { + Preconditions.checkArgument(chatMessageUpload != null); + Preconditions.checkArgument(chatMessageUpload.getChatMessage() != null); + Preconditions.checkArgument(chatMessageUpload.getFromPlayer() != null); + Preconditions.checkArgument(chatMessageUpload.getGameId() != null); + + Preconditions.checkArgument( + chatMessageUpload.getFromPlayer().length() <= LobbyConstants.USERNAME_MAX_LENGTH); + + String authHeader = request.getHeader("Authorization"); + + if (!chatUploadModule.upload( + // truncate 'Bearer ' from the auth token if present + authHeader.startsWith("Bearer ") ? authHeader.substring("Bearer ".length()) : authHeader, + chatMessageUpload)) { + log.warn( + "Chat upload request from {} was rejected, " + + "gameID and API-key pair did not match any existing games.", + request.getRemoteHost()); + } + + return Response.ok().build(); + } + + @POST + @Path(LobbyWatcherClient.PLAYER_JOINED_PATH) + @RolesAllowed(UserRole.HOST) + public Response playerJoinedGame( + @Auth final AuthenticatedUser authenticatedUser, + final PlayerJoinedNotification playerJoinedNotification) { + gameListing.addPlayerToGame( + UserName.of(playerJoinedNotification.getPlayerName()), + authenticatedUser.getApiKey(), + playerJoinedNotification.getGameId()); + + return Response.ok().build(); + } + + @POST + @Path(LobbyWatcherClient.PLAYER_LEFT_PATH) + @RolesAllowed(UserRole.HOST) + public Response playerLeftGame( + @Auth final AuthenticatedUser authenticatedUser, + final PlayerLeftNotification playerLeftNotification) { + + gameListing.removePlayerFromGame( + UserName.of(playerLeftNotification.getPlayerName()), + authenticatedUser.getApiKey(), + playerLeftNotification.getGameId()); + + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/PlayersInGameController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/PlayersInGameController.java new file mode 100644 index 0000000..6b0e5df --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/PlayersInGameController.java @@ -0,0 +1,32 @@ +package org.triplea.spitfire.server.controllers.lobby; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import java.util.Collection; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import lombok.AllArgsConstructor; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.player.PlayerLobbyActionsClient; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +@RolesAllowed(UserRole.ANONYMOUS) +@AllArgsConstructor +public class PlayersInGameController extends HttpController { + private final GameListing gameListing; + + public static PlayersInGameController build(final GameListing gameListing) { + return new PlayersInGameController(gameListing); + } + + @POST + @Path(PlayerLobbyActionsClient.FETCH_PLAYERS_IN_GAME) + public Collection fetchPlayersInGame( + @Auth final AuthenticatedUser authenticatedUser, final String gameId) { + Preconditions.checkNotNull(gameId); + return gameListing.getPlayersInGame(gameId); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/AccessLogController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/AccessLogController.java new file mode 100644 index 0000000..d4f10a8 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/AccessLogController.java @@ -0,0 +1,37 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogRequest; +import org.triplea.http.client.lobby.moderator.toolbox.log.ToolboxAccessLogClient; +import org.triplea.modules.moderation.access.log.AccessLogService; +import org.triplea.spitfire.server.HttpController; + +/** Controller to query the access log table, for us by moderators. */ +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class AccessLogController extends HttpController { + @Nonnull private final AccessLogService accessLogService; + + public static AccessLogController build(final Jdbi jdbi) { + return AccessLogController.builder() // + .accessLogService(AccessLogService.build(jdbi)) + .build(); + } + + @POST + @Path(ToolboxAccessLogClient.FETCH_ACCESS_LOG_PATH) + public Response fetchAccessLog(final AccessLogRequest accessLogRequest) { + Preconditions.checkNotNull(accessLogRequest); + Preconditions.checkNotNull(accessLogRequest.getAccessLogSearchRequest()); + Preconditions.checkNotNull(accessLogRequest.getPagingParams()); + return Response.ok().entity(accessLogService.fetchAccessLog(accessLogRequest)).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/BadWordsController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/BadWordsController.java new file mode 100644 index 0000000..4a0bfcf --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/BadWordsController.java @@ -0,0 +1,67 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.words.ToolboxBadWordsClient; +import org.triplea.modules.moderation.bad.words.BadWordsService; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** Controller for servicing moderator toolbox bad-words tab (provides CRUD operations). */ +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class BadWordsController extends HttpController { + @Nonnull private final BadWordsService badWordsService; + + public static BadWordsController build(final Jdbi jdbi) { + return BadWordsController.builder() // + .badWordsService(BadWordsService.build(jdbi)) + .build(); + } + + /** + * Removes a bad word entry from the bad-word table. + * + * @param word The new bad word entry to remove. We expect this to exist in the table or else we + * return a 400. + */ + @POST + @Path(ToolboxBadWordsClient.BAD_WORD_REMOVE_PATH) + public Response removeBadWord( + @Auth final AuthenticatedUser authenticatedUser, final String word) { + Preconditions.checkArgument(word != null && !word.isEmpty()); + badWordsService.removeBadWord(authenticatedUser.getUserIdOrThrow(), word); + return Response.ok().entity("Removed bad word: " + word).build(); + } + + /** + * Adds a bad word entry to the bad-word table. + * + * @param word The new bad word entry to add. + */ + @POST + @Path(ToolboxBadWordsClient.BAD_WORD_ADD_PATH) + public Response addBadWord(@Auth final AuthenticatedUser authenticatedUser, final String word) { + Preconditions.checkArgument(word != null && !word.isEmpty()); + return badWordsService.addBadWord(authenticatedUser.getUserIdOrThrow(), word) + ? Response.ok().build() + : Response.status(400) + .entity(word + " was not added, it may already have been added") + .build(); + } + + @GET + @Path(ToolboxBadWordsClient.BAD_WORD_GET_PATH) + public Response getBadWords() { + return Response.status(200).entity(badWordsService.getBadWords()).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/DisconnectUserController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/DisconnectUserController.java new file mode 100644 index 0000000..1a9836a --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/DisconnectUserController.java @@ -0,0 +1,44 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.moderation.disconnect.user.DisconnectUserAction; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; +import org.triplea.web.socket.WebSocketMessagingBus; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RolesAllowed(UserRole.MODERATOR) +public class DisconnectUserController extends HttpController { + + private final DisconnectUserAction disconnectUserAction; + + public static DisconnectUserController build( + final Jdbi jdbi, final Chatters chatters, final WebSocketMessagingBus playerConnections) { + return new DisconnectUserController( + DisconnectUserAction.build(jdbi, chatters, playerConnections)); + } + + @POST + @Path(ModeratorLobbyClient.DISCONNECT_PLAYER_PATH) + public Response disconnectPlayer( + @Auth final AuthenticatedUser authenticatedUser, final String playerIdToBan) { + Preconditions.checkNotNull(playerIdToBan); + + final boolean removed = + disconnectUserAction.disconnectPlayer( + authenticatedUser.getUserIdOrThrow(), PlayerChatId.of(playerIdToBan)); + return Response.status(removed ? 200 : 400).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/GameChatHistoryController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/GameChatHistoryController.java new file mode 100644 index 0000000..5b1a40d --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/GameChatHistoryController.java @@ -0,0 +1,32 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import java.util.List; +import java.util.function.Function; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.ChatHistoryMessage; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.modules.moderation.chat.history.FetchGameChatHistoryModule; +import org.triplea.spitfire.server.HttpController; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RolesAllowed(UserRole.MODERATOR) +public class GameChatHistoryController extends HttpController { + + private final Function> fetchChatHistoryAction; + + public static GameChatHistoryController build(final Jdbi jdbi) { + return new GameChatHistoryController(FetchGameChatHistoryModule.build(jdbi)); + } + + @POST + @Path(ModeratorLobbyClient.FETCH_GAME_CHAT_HISTORY) + public List fetchGameChatHistory(final String gameId) { + return fetchChatHistoryAction.apply(gameId); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorAuditHistoryController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorAuditHistoryController.java new file mode 100644 index 0000000..c359a4f --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorAuditHistoryController.java @@ -0,0 +1,49 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.PagingParams; +import org.triplea.http.client.lobby.moderator.toolbox.log.ModeratorEvent; +import org.triplea.http.client.lobby.moderator.toolbox.log.ToolboxEventLogClient; +import org.triplea.modules.moderation.audit.history.ModeratorAuditHistoryService; +import org.triplea.spitfire.server.HttpController; + +/** Http server endpoints for accessing and returning moderator audit history rows. */ +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class ModeratorAuditHistoryController extends HttpController { + @Nonnull private final ModeratorAuditHistoryService moderatorAuditHistoryService; + + public static ModeratorAuditHistoryController build(final Jdbi jdbi) { + return ModeratorAuditHistoryController.builder() + .moderatorAuditHistoryService(ModeratorAuditHistoryService.build(jdbi)) + .build(); + } + + /** + * Use this method to retrieve moderator audit history rows. Presents a paged interface. + * + * @param pagingParams Parameter JSON object for page number and page size. + */ + @POST + @Path(ToolboxEventLogClient.AUDIT_HISTORY_PATH) + public Response lookupAuditHistory(final PagingParams pagingParams) { + Preconditions.checkArgument(pagingParams != null); + Preconditions.checkArgument(pagingParams.getRowNumber() >= 0); + Preconditions.checkArgument(pagingParams.getPageSize() > 0); + + final List moderatorEvents = + moderatorAuditHistoryService.lookupHistory( + pagingParams.getRowNumber(), pagingParams.getPageSize()); + + return Response.status(200).entity(moderatorEvents).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorsController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorsController.java new file mode 100644 index 0000000..c11e146 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/ModeratorsController.java @@ -0,0 +1,80 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.management.ToolboxModeratorManagementClient; +import org.triplea.modules.moderation.moderators.ModeratorsService; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** + * Provides endpoint for moderator maintenance actions and to support the moderators toolbox + * 'moderators' tab. Actions include: adding moderators, removing moderators, and promoting + * moderators to 'super-mod'. Some actions are only allowed for super-mods. + */ +@Builder +public class ModeratorsController extends HttpController { + @Nonnull private final ModeratorsService moderatorsService; + + /** Factory method , instantiates {@code ModeratorsController} with dependencies. */ + public static ModeratorsController build(final Jdbi jdbi) { + return ModeratorsController.builder() // + .moderatorsService(ModeratorsService.build(jdbi)) + .build(); + } + + @POST + @Path(ToolboxModeratorManagementClient.CHECK_USER_EXISTS_PATH) + @RolesAllowed(UserRole.ADMIN) + public Response checkUserExists(final String username) { + return Response.ok().entity(moderatorsService.userExistsByName(username)).build(); + } + + @GET + @Path(ToolboxModeratorManagementClient.FETCH_MODERATORS_PATH) + @RolesAllowed(UserRole.MODERATOR) + public Response getModerators() { + return Response.ok().entity(moderatorsService.fetchModerators()).build(); + } + + @GET + @Path(ToolboxModeratorManagementClient.IS_ADMIN_PATH) + public Response isAdmin(@Auth final AuthenticatedUser authenticatedUser) { + return Response.ok().entity(authenticatedUser.getUserRole().equals(UserRole.ADMIN)).build(); + } + + @POST + @Path(ToolboxModeratorManagementClient.REMOVE_MOD_PATH) + @RolesAllowed(UserRole.ADMIN) + public Response removeMod( + @Auth final AuthenticatedUser authenticatedUser, final String moderatorName) { + moderatorsService.removeMod(authenticatedUser.getUserIdOrThrow(), moderatorName); + return Response.ok().build(); + } + + @POST + @Path(ToolboxModeratorManagementClient.ADD_ADMIN_PATH) + @RolesAllowed(UserRole.ADMIN) + public Response setAdmin( + @Auth final AuthenticatedUser authenticatedUser, final String moderatorName) { + moderatorsService.addAdmin(authenticatedUser.getUserIdOrThrow(), moderatorName); + return Response.ok().build(); + } + + @POST + @Path(ToolboxModeratorManagementClient.ADD_MODERATOR_PATH) + @RolesAllowed(UserRole.ADMIN) + public Response addModerator( + @Auth final AuthenticatedUser authenticatedUser, final String username) { + moderatorsService.addModerator(authenticatedUser.getUserIdOrThrow(), username); + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/MuteUserController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/MuteUserController.java new file mode 100644 index 0000000..072e279 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/MuteUserController.java @@ -0,0 +1,39 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.http.client.lobby.moderator.MuteUserRequest; +import org.triplea.modules.chat.Chatters; +import org.triplea.spitfire.server.HttpController; + +@AllArgsConstructor +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class MuteUserController extends HttpController { + + private final Chatters chatters; + + public static MuteUserController build(final Chatters chatters) { + return new MuteUserController(chatters); + } + + @POST + @Path(ModeratorLobbyClient.MUTE_USER) + public Response muteUser(final MuteUserRequest muteUserRequest) { + Preconditions.checkArgument(muteUserRequest != null); + Preconditions.checkArgument(muteUserRequest.getPlayerChatId() != null); + Preconditions.checkArgument(muteUserRequest.getMinutes() > 0); + + chatters.mutePlayer( + PlayerChatId.of(muteUserRequest.getPlayerChatId()), muteUserRequest.getMinutes()); + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/RemoteActionsController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/RemoteActionsController.java new file mode 100644 index 0000000..d24b34b --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/RemoteActionsController.java @@ -0,0 +1,58 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.remote.actions.RemoteActionsClient; +import org.triplea.java.ArgChecker; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.moderation.remote.actions.RemoteActionsModule; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** + * Endpoints for moderators to use to issue remote action commands that affect game-hosts, eg: + * requesting a server to shutdown. + */ +@Builder +public class RemoteActionsController extends HttpController { + @Nonnull private final RemoteActionsModule remoteActionsModule; + + public static RemoteActionsController build( + final Jdbi jdbi, final WebSocketMessagingBus gameMessagingBus) { + return RemoteActionsController.builder() + .remoteActionsModule(RemoteActionsModule.build(jdbi, gameMessagingBus)) + .build(); + } + + @POST + @Path(RemoteActionsClient.SEND_SHUTDOWN_PATH) + @RolesAllowed(UserRole.MODERATOR) + public Response sendShutdownSignal( + @Auth final AuthenticatedUser authenticatedUser, final String gameId) { + Preconditions.checkNotNull(gameId); + + remoteActionsModule.addGameIdForShutdown(authenticatedUser.getUserIdOrThrow(), gameId); + return Response.ok().build(); + } + + @POST + @Path(RemoteActionsClient.IS_PLAYER_BANNED_PATH) + @RolesAllowed(UserRole.HOST) + public Response isUserBanned(final String ipAddress) { + ArgChecker.checkNotEmpty(ipAddress); + Preconditions.checkArgument(IpAddressParser.isValid(ipAddress)); + + final boolean result = remoteActionsModule.isUserBanned(IpAddressParser.fromString(ipAddress)); + + return Response.ok().entity(result).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UserBanController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UserBanController.java new file mode 100644 index 0000000..17752e1 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UserBanController.java @@ -0,0 +1,89 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.BanPlayerRequest; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.ToolboxUserBanClient; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanParams; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.moderation.ban.user.UserBanService; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** Controller for endpoints to manage user bans, to be used by moderators. */ +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class UserBanController extends HttpController { + @Nonnull private final UserBanService bannedUsersService; + + public static UserBanController build( + final Jdbi jdbi, + final Chatters chatters, + final WebSocketMessagingBus chatMessagingBus, + final WebSocketMessagingBus gameMessagingBus) { + return UserBanController.builder() + .bannedUsersService( + UserBanService.builder() + .jdbi(jdbi) + .chatters(chatters) + .chatMessagingBus(chatMessagingBus) + .gameMessagingBus(gameMessagingBus) + .build()) + .build(); + } + + @GET + @Path(ToolboxUserBanClient.GET_USER_BANS_PATH) + public Response getUserBans() { + return Response.ok().entity(bannedUsersService.getBannedUsers()).build(); + } + + @POST + @Path(ToolboxUserBanClient.REMOVE_USER_BAN_PATH) + public Response removeUserBan( + @Auth final AuthenticatedUser authenticatedUser, final String banId) { + Preconditions.checkArgument(banId != null); + + final boolean removed = + bannedUsersService.removeUserBan(authenticatedUser.getUserIdOrThrow(), banId); + return Response.status(removed ? 200 : 400).build(); + } + + /** Endpoint to add a user ban. Returns 200 if the ban is added, 400 if not. */ + @POST + @Path(ToolboxUserBanClient.BAN_USER_PATH) + public Response banUser( + @Auth final AuthenticatedUser authenticatedUser, final UserBanParams banUserParams) { + Preconditions.checkArgument(banUserParams != null); + Preconditions.checkArgument(banUserParams.getSystemId() != null); + Preconditions.checkArgument(banUserParams.getIp() != null); + Preconditions.checkArgument(banUserParams.getUsername() != null); + Preconditions.checkArgument(banUserParams.getMinutesToBan() > 0); + + bannedUsersService.banUser(authenticatedUser.getUserIdOrThrow(), banUserParams); + return Response.ok().build(); + } + + @POST + @Path(ModeratorLobbyClient.BAN_PLAYER_PATH) + public Response banPlayer( + @Auth final AuthenticatedUser authenticatedUser, final BanPlayerRequest banPlayerRequest) { + Preconditions.checkNotNull(banPlayerRequest); + Preconditions.checkNotNull(banPlayerRequest.getPlayerChatId()); + Preconditions.checkArgument(banPlayerRequest.getBanMinutes() > 0); + + bannedUsersService.banUser(authenticatedUser.getUserIdOrThrow(), banPlayerRequest); + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UsernameBanController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UsernameBanController.java new file mode 100644 index 0000000..c87cf9e --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/lobby/moderation/UsernameBanController.java @@ -0,0 +1,61 @@ +package org.triplea.spitfire.server.controllers.lobby.moderation; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.banned.name.ToolboxUsernameBanClient; +import org.triplea.modules.moderation.ban.name.UsernameBanService; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** Endpoint for use by moderators to view, add and remove player username bans. */ +@Builder +@RolesAllowed(UserRole.MODERATOR) +public class UsernameBanController extends HttpController { + @Nonnull private final UsernameBanService bannedNamesService; + + public static UsernameBanController build(final Jdbi jdbi) { + return UsernameBanController.builder() + .bannedNamesService(UsernameBanService.build(jdbi)) + .build(); + } + + @POST + @Path(ToolboxUsernameBanClient.REMOVE_BANNED_USER_NAME_PATH) + public Response removeBannedUsername( + @Auth final AuthenticatedUser authenticatedUser, final String username) { + Preconditions.checkArgument(username != null && !username.isEmpty()); + return Response.status( + bannedNamesService.removeUsernameBan(authenticatedUser.getUserIdOrThrow(), username) + ? 200 + : 400) + .build(); + } + + @POST + @Path(ToolboxUsernameBanClient.ADD_BANNED_USER_NAME_PATH) + public Response addBannedUsername( + @Auth final AuthenticatedUser authenticatedUser, final String username) { + Preconditions.checkArgument(username != null && !username.isEmpty()); + return Response.status( + bannedNamesService.addBannedUserName( + authenticatedUser.getUserIdOrThrow(), username.toUpperCase()) + ? 200 + : 400) + .build(); + } + + @GET + @Path(ToolboxUsernameBanClient.GET_BANNED_USER_NAMES_PATH) + public Response getBannedUsernames() { + return Response.status(200).entity(bannedNamesService.getBannedUserNames()).build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/CreateAccountController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/CreateAccountController.java new file mode 100644 index 0000000..f98f7d8 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/CreateAccountController.java @@ -0,0 +1,47 @@ +package org.triplea.spitfire.server.controllers.user.account; + +import com.google.common.base.Preconditions; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.domain.data.LobbyConstants; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.http.client.lobby.login.CreateAccountResponse; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.modules.user.account.create.CreateAccountModule; +import org.triplea.spitfire.server.HttpController; + +@Builder +public class CreateAccountController extends HttpController { + + @Nonnull private final Function createAccountModule; + + public static CreateAccountController build(final Jdbi jdbi) { + return CreateAccountController.builder() + .createAccountModule(CreateAccountModule.build(jdbi)) + .build(); + } + + @POST + @Path(LobbyLoginClient.CREATE_ACCOUNT) + public CreateAccountResponse createAccount(final CreateAccountRequest createAccountRequest) { + Preconditions.checkArgument(createAccountRequest != null); + Preconditions.checkArgument(createAccountRequest.getUsername() != null); + Preconditions.checkArgument( + createAccountRequest.getUsername().length() <= LobbyConstants.USERNAME_MAX_LENGTH); + Preconditions.checkArgument( + createAccountRequest.getUsername().length() >= LobbyConstants.USERNAME_MIN_LENGTH); + Preconditions.checkArgument(createAccountRequest.getEmail() != null); + Preconditions.checkArgument( + createAccountRequest.getEmail().length() <= LobbyConstants.EMAIL_MAX_LENGTH); + Preconditions.checkArgument(createAccountRequest.getPassword() != null); + Preconditions.checkArgument( + createAccountRequest.getPassword().length() >= LobbyConstants.PASSWORD_MIN_LENGTH); + Preconditions.checkArgument(createAccountRequest.getEmail() != null); + + return createAccountModule.apply(createAccountRequest); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/ForgotPasswordController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/ForgotPasswordController.java new file mode 100644 index 0000000..a032cea --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/ForgotPasswordController.java @@ -0,0 +1,55 @@ +package org.triplea.spitfire.server.controllers.user.account; + +import com.google.common.annotations.VisibleForTesting; +import java.util.function.BiFunction; +import javax.annotation.Nonnull; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.dropwizard.common.IpAddressExtractor; +import org.triplea.http.client.forgot.password.ForgotPasswordClient; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; +import org.triplea.http.client.forgot.password.ForgotPasswordResponse; +import org.triplea.modules.LobbyModuleConfig; +import org.triplea.modules.forgot.password.ForgotPasswordModule; +import org.triplea.spitfire.server.HttpController; + +/** Http controller that binds the error upload endpoint with the error report upload handler. */ +@Builder +@AllArgsConstructor( + access = AccessLevel.PACKAGE, + onConstructor_ = {@VisibleForTesting}) +public class ForgotPasswordController extends HttpController { + @Nonnull private final BiFunction forgotPasswordModule; + + public static ForgotPasswordController build( + final LobbyModuleConfig lobbyModuleConfig, final Jdbi jdbi) { + return ForgotPasswordController.builder() + .forgotPasswordModule( + ForgotPasswordModule.build( + lobbyModuleConfig.isGameHostConnectivityCheckEnabled(), jdbi)) + .build(); + } + + @POST + @Path(ForgotPasswordClient.FORGOT_PASSWORD_PATH) + public ForgotPasswordResponse requestTempPassword( + @Context final HttpServletRequest request, + final ForgotPasswordRequest forgotPasswordRequest) { + + if (forgotPasswordRequest.getUsername() == null || forgotPasswordRequest.getEmail() == null) { + throw new IllegalArgumentException("Missing username or email in request"); + } + + return ForgotPasswordResponse.builder() + .responseMessage( + forgotPasswordModule.apply( + IpAddressExtractor.extractIpAddress(request), forgotPasswordRequest)) + .build(); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/LoginController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/LoginController.java new file mode 100644 index 0000000..19b66bf --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/LoginController.java @@ -0,0 +1,49 @@ +package org.triplea.spitfire.server.controllers.user.account; + +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.domain.data.LobbyConstants; +import org.triplea.dropwizard.common.IpAddressExtractor; +import org.triplea.http.client.LobbyHttpClientConfig; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.http.client.lobby.login.LoginRequest; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.user.account.login.LoginModule; +import org.triplea.spitfire.server.HttpController; + +@Builder +@AllArgsConstructor +public class LoginController extends HttpController { + @Nonnull private final LoginModule loginModule; + + public static LoginController build(final Jdbi jdbi, final Chatters chatters) { + return LoginController.builder() // + .loginModule(LoginModule.build(jdbi, chatters)) + .build(); + } + + @POST + @Path(LobbyLoginClient.LOGIN_PATH) + public LobbyLoginResponse login( + @Context final HttpServletRequest request, final LoginRequest loginRequest) { + Preconditions.checkArgument(loginRequest != null); + Preconditions.checkArgument(loginRequest.getName() != null); + Preconditions.checkArgument( + loginRequest.getName().length() <= LobbyConstants.USERNAME_MAX_LENGTH); + Preconditions.checkArgument( + loginRequest.getName().length() >= LobbyConstants.USERNAME_MIN_LENGTH); + + return loginModule.doLogin( + loginRequest, + request.getHeader(LobbyHttpClientConfig.SYSTEM_ID_HEADER), + IpAddressExtractor.extractIpAddress(request)); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/PlayerInfoController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/PlayerInfoController.java new file mode 100644 index 0000000..e9b1399 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/PlayerInfoController.java @@ -0,0 +1,41 @@ +package org.triplea.spitfire.server.controllers.user.account; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.PlayerSummary; +import org.triplea.http.client.lobby.player.PlayerLobbyActionsClient; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.modules.player.info.FetchPlayerInfoModule; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RolesAllowed(UserRole.ANONYMOUS) +public class PlayerInfoController extends HttpController { + + private final FetchPlayerInfoModule fetchPlayerInfoModule; + + public static PlayerInfoController build( + final Jdbi jdbi, final Chatters chatters, final GameListing gameListing) { + return new PlayerInfoController(FetchPlayerInfoModule.build(jdbi, chatters, gameListing)); + } + + @POST + @Path(PlayerLobbyActionsClient.FETCH_PLAYER_INFORMATION) + public PlayerSummary fetchPlayerInfo( + @Auth final AuthenticatedUser authenticatedUser, final String playerId) { + Preconditions.checkNotNull(playerId); + return UserRole.isModerator(authenticatedUser.getUserRole()) + ? fetchPlayerInfoModule.fetchPlayerInfoAsModerator(PlayerChatId.of(playerId)) + : fetchPlayerInfoModule.fetchPlayerInfo(PlayerChatId.of(playerId)); + } +} diff --git a/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/UpdateAccountController.java b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/UpdateAccountController.java new file mode 100644 index 0000000..d5814a0 --- /dev/null +++ b/server/dropwizard-server/src/main/java/org/triplea/spitfire/server/controllers/user/account/UpdateAccountController.java @@ -0,0 +1,66 @@ +package org.triplea.spitfire.server.controllers.user.account; + +import com.google.common.base.Preconditions; +import io.dropwizard.auth.Auth; +import javax.annotation.Nonnull; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.user.account.FetchEmailResponse; +import org.triplea.http.client.lobby.user.account.UserAccountClient; +import org.triplea.java.ArgChecker; +import org.triplea.modules.user.account.update.UpdateAccountService; +import org.triplea.spitfire.server.HttpController; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +/** Controller providing endpoints for user account management. */ +@Builder +public class UpdateAccountController extends HttpController { + @Nonnull private final UpdateAccountService userAccountService; + + /** Instantiates controller with dependencies. */ + public static UpdateAccountController build(final Jdbi jdbi) { + return UpdateAccountController.builder() + .userAccountService(UpdateAccountService.build(jdbi)) + .build(); + } + + @POST + @Path(UserAccountClient.CHANGE_PASSWORD_PATH) + @RolesAllowed(UserRole.PLAYER) + public Response changePassword( + @Auth final AuthenticatedUser authenticatedUser, final String newPassword) { + ArgChecker.checkNotEmpty(newPassword); + Preconditions.checkArgument(authenticatedUser.getUserIdOrThrow() > 0); + + userAccountService.changePassword(authenticatedUser.getUserIdOrThrow(), newPassword); + return Response.ok().build(); + } + + @GET + @Path(UserAccountClient.FETCH_EMAIL_PATH) + @RolesAllowed(UserRole.PLAYER) + public FetchEmailResponse fetchEmail(@Auth final AuthenticatedUser authenticatedUser) { + Preconditions.checkArgument(authenticatedUser.getUserIdOrThrow() > 0); + + return new FetchEmailResponse( + userAccountService.fetchEmail(authenticatedUser.getUserIdOrThrow())); + } + + @POST + @Path(UserAccountClient.CHANGE_EMAIL_PATH) + @RolesAllowed(UserRole.PLAYER) + public Response changeEmail( + @Auth final AuthenticatedUser authenticatedUser, final String newEmail) { + ArgChecker.checkNotEmpty(newEmail); + Preconditions.checkArgument(authenticatedUser.getUserIdOrThrow() > 0); + + userAccountService.changeEmail(authenticatedUser.getUserIdOrThrow(), newEmail); + return Response.ok().build(); + } +} diff --git a/server/dropwizard-server/src/main/resources/configuration.yml b/server/dropwizard-server/src/main/resources/configuration.yml new file mode 100644 index 0000000..f6f08b6 --- /dev/null +++ b/server/dropwizard-server/src/main/resources/configuration.yml @@ -0,0 +1,55 @@ +githubApiToken: ${GITHUB_API_TOKEN:-} +githubWebServiceUrl: https://api.github.com +githubGameOrg: triplea-game + +latestVersionFetcherEnabled: ${LATEST_VERSION_FETCHER_ENABLED:-true} + +# this is to help guarantee that we do not accidentally use test configuration in prod. +gameHostConnectivityCheckEnabled: ${GAME_HOST_CONNECTIVITY_CHECK_ENABLED:-false} + +# Whether to print out SQL statements as executed, useful for debugging. +logSqlStatements: false + +database: + driverClass: org.postgresql.Driver + user: ${DATABASE_USER:-lobby_user} + password: ${DATABASE_PASSWORD:-lobby} + url: jdbc:postgresql://${DB_URL:-localhost:5432/lobby_db} + properties: + charSet: UTF-8 + # the maximum amount of time to wait on an empty pool before throwing an exception + maxWaitForConnection: 1s + + # the SQL query to run when validating a connection's liveness + validationQuery: select 1 + + # the minimum number of connections to keep open + minSize: 8 + + # the maximum number of connections to keep open + maxSize: 32 + + # whether or not idle connections should be validated + checkConnectionWhileIdle: false + + # the amount of time to sleep between runs of the idle connection validation, abandoned cleaner and idle pool resizing + evictionInterval: 10s + + # the minimum amount of time an connection must sit idle in the pool before it is eligible for eviction + minIdleTime: 1 minute + + +logging: + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + level: INFO + loggers: + # Set this to DEBUG to troubleshoot HTTP 400 "Unable to process JSON" errors. + io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper: INFO + +server: + applicationConnectors: + - type: http + port: ${HTTP_PORT:-8080} + # useForwardedHeaders is important for when behind a reverse proxy (NGINX) + useForwardedHeaders: true + adminConnectors: [] diff --git a/server/dropwizard-server/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java b/server/dropwizard-server/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java new file mode 100644 index 0000000..086d6e0 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java @@ -0,0 +1,47 @@ +package org.triplea.dropwizard.common; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class IpAddressExtractorTest { + + private static final String IPV4 = "127.0.0.1"; + private static final String IPV6 = "3ffe:1900:4545:3:200:f8ff:fe21:67cf"; + + @Mock private HttpServletRequest httpServletRequest; + + @Test + void ipv6() { + when(httpServletRequest.getRemoteAddr()).thenReturn(IPV6); + assertThat( + "Normal formatted IPv6 as input should return same value", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV6)); + } + + @Test + void ipv6_WithBrackets() { + when(httpServletRequest.getRemoteAddr()).thenReturn("[" + IPV6 + "]"); + assertThat( + "Square brackets on the IPv6 should be stripped", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV6)); + } + + @Test + void ipv4() { + when(httpServletRequest.getRemoteAddr()).thenReturn(IPV4); + assertThat( + "IPv4 format is returned unaltered", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV4)); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/ControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/ControllerIntegrationTest.java new file mode 100644 index 0000000..8743fa3 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/ControllerIntegrationTest.java @@ -0,0 +1,114 @@ +package org.triplea.spitfire.server; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.fail; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import com.google.common.base.Preconditions; +import feign.FeignException; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.SystemIdLoader; +import org.triplea.http.client.LobbyHttpClientConfig; +import org.triplea.test.common.RequiresDatabase; + +@ExtendWith(SpitfireServerTestExtension.class) +@ExtendWith(SpitfireDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@DataSet(value = ControllerIntegrationTest.DATA_SETS, useSequenceFiltering = false) +@RequiresDatabase +public abstract class ControllerIntegrationTest { + + @BeforeAll + static void setup() { + LobbyHttpClientConfig.setConfig( + LobbyHttpClientConfig.builder() + .clientVersion("1.0") + .systemId(SystemIdLoader.load().getValue()) + .build()); + } + + @AfterAll + static void tearDown() { + LobbyHttpClientConfig.setConfig(null); + } + + /** + * All data sets used for controller integration tests. Note, we include all data even that which + * is not used because the tests rely on just a single static data set. If we were doing more + * thorough DB level testing we would want different variations of the same data. In this case we + * include everything to keep test configuration easy, notably so each test does not need to + * define the data it needs. + */ + public static final String DATA_SETS = + "user_role.yml," + + "lobby_user.yml," + + "lobby_api_key.yml," + + "access_log.yml," + + "bad_word.yml," + + "banned_username.yml," + + "banned_user.yml," + + "game_hosting_api_key.yml," + + "map_index.yml," + + "map_tag_value.yml," + + "moderator_action_history.yml," + + "temp_password_request.yml" + + ""; + + public static final ApiKey ADMIN = ApiKey.of("ADMIN"); + public static final ApiKey MODERATOR = ApiKey.of("MODERATOR"); + public static final ApiKey PLAYER = ApiKey.of("PLAYER"); + public static final ApiKey ANONYMOUS = ApiKey.of("ANONYMOUS"); + public static final ApiKey HOST = ApiKey.of("HOST"); + + public static final List NOT_MODERATORS = List.of(ANONYMOUS, PLAYER); + public static final List NOT_HOST = List.of(ANONYMOUS, PLAYER, ADMIN, MODERATOR); + + /** + * Loops over a collection of api keys each one being used to create a client, we then iterate + * over each invocation and invoke it using each client. For each invocation we verify we get an + * {@code HttpInteractionException} with status '403' (not authorized). + * + * @param keys The API keys expected to not have authorization + * @param clientBuilder Function taking an API key and returning a client + * @param clientInvocations Consumer collection that accepts a client we constructed and should do + * an API call where we would expect a 403 using that client. + */ + public static void assertNotAuthorized( + final Collection keys, + final Function clientBuilder, + final Consumer... clientInvocations) { + Preconditions.checkArgument(!keys.isEmpty()); + Preconditions.checkArgument(clientInvocations.length > 0); + + for (final ApiKey key : keys) { + final T client = clientBuilder.apply(key); + + for (final Consumer invocation : clientInvocations) { + try { + invocation.accept(client); + fail("Invocation did not produce a 403"); + } catch (final FeignException feignException) { + assertThat(feignException.status(), is(403)); + } + } + } + } + + public static void assertBadRequest(final Runnable invocation) { + try { + invocation.run(); + fail("Invocation did not produce a 400"); + } catch (final FeignException feignException) { + assertThat(feignException.status(), is(400)); + } + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/DropwizardServerExtension.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/DropwizardServerExtension.java new file mode 100644 index 0000000..3fe791b --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/DropwizardServerExtension.java @@ -0,0 +1,77 @@ +package org.triplea.spitfire.server; + +import com.google.common.base.Preconditions; +import io.dropwizard.Configuration; +import io.dropwizard.testing.DropwizardTestSupport; +import java.net.URI; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * Extension to start a dropwizard server. This extension can read the 'configuration.yml' of the + * server which allows access to database connectivity parameters. Tests can have several objects + * injected into them by declaring those objects as constructor or test method parameters. Those + * objects are: + * + *

+ * + * @param Server configuration type. + */ +public abstract class DropwizardServerExtension + implements BeforeAllCallback, ParameterResolver { + + private static URI serverUri; + + /** + * Implementations should return a *static* instance of DropwizardTestSupport. If returning a + * local instance, the test server may be turned off after a single test class is done executing + * and subsequent tests could fail due to server not being on. Example implementation + * + *
{@code
+   * @Getter
+   * static DropwizardTestSupport testSupport =
+   *   new DropwizardTestSupport<>(MapsServer.class, "configuration.yml")
+   * }
+ */ + protected abstract DropwizardTestSupport getSupport(); + + @Override + public void beforeAll(final ExtensionContext context) throws Exception { + final DropwizardTestSupport support = getSupport(); + support.before(); + + final String localUri = "http://localhost:" + support.getLocalPort(); + serverUri = URI.create(localUri); + } + + @Override + public boolean supportsParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType().equals(URI.class) + || parameterContext + .getParameter() + .getType() + .equals(getSupport().getConfiguration().getClass()); + } + + @Override + public Object resolveParameter( + final ParameterContext parameterContext, final ExtensionContext extensionContext) + throws ParameterResolutionException { + if (parameterContext.getParameter().getType().equals(URI.class)) { + return Preconditions.checkNotNull(serverUri); + } else { + return getSupport().getConfiguration(); + } + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireDatabaseTestSupport.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireDatabaseTestSupport.java new file mode 100644 index 0000000..3590989 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireDatabaseTestSupport.java @@ -0,0 +1,18 @@ +package org.triplea.spitfire.server; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.jdbi.v3.core.mapper.RowMapperFactory; +import org.triplea.db.LobbyModuleRowMappers; +import org.triplea.spitfire.database.DatabaseTestSupport; + +public class SpitfireDatabaseTestSupport extends DatabaseTestSupport { + + @Override + protected Collection rowMappers() { + final List rowMappers = new ArrayList<>(); + rowMappers.addAll(LobbyModuleRowMappers.rowMappers()); + return rowMappers; + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireServerTestExtension.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireServerTestExtension.java new file mode 100644 index 0000000..d22b884 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/SpitfireServerTestExtension.java @@ -0,0 +1,19 @@ +package org.triplea.spitfire.server; + +import io.dropwizard.testing.DropwizardTestSupport; + +/** + * Use with {@code @ExtendWith(SpitfireServerTestExtension.class)} Tests extended with this + * extension will launch a drop wizard server when the test starts up. If a server is already + * running at the start of a test, it will be re-used. + */ +public class SpitfireServerTestExtension extends DropwizardServerExtension { + + private static final DropwizardTestSupport testSupport = + new DropwizardTestSupport<>(SpitfireServerApplication.class, "configuration.yml"); + + @Override + public DropwizardTestSupport getSupport() { + return testSupport; + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/TestData.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/TestData.java new file mode 100644 index 0000000..0967df3 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/TestData.java @@ -0,0 +1,25 @@ +package org.triplea.spitfire.server; + +import java.time.Instant; +import lombok.experimental.UtilityClass; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.LobbyGame; + +@UtilityClass +public class TestData { + public static final ApiKey API_KEY = ApiKey.of("test"); + + public static final LobbyGame LOBBY_GAME = + LobbyGame.builder() + .hostAddress("127.0.0.1") + .hostPort(12) + .hostName("name") + .mapName("map") + .playerCount(3) + .gameRound(1) + .epochMilliTimeStarted(Instant.now().toEpochMilli()) + .passworded(false) + .status("Waiting For Players") + .comments("comments") + .build(); +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticatorTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticatorTest.java new file mode 100644 index 0000000..db2310e --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/ApiKeyAuthenticatorTest.java @@ -0,0 +1,78 @@ +package org.triplea.spitfire.server.access.authentication; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.GameHostingApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerApiKeyLookupRecord; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; +import org.triplea.spitfire.server.TestData; + +@ExtendWith(MockitoExtension.class) +class ApiKeyAuthenticatorTest { + private static final ApiKey API_KEY = TestData.API_KEY; + + private static final PlayerApiKeyLookupRecord PLAYER_DATA = + PlayerApiKeyLookupRecord.builder() + .username("player-name") + .userRole(UserRole.PLAYER) + .userId(100) + .apiKeyId(123) + .playerChatId("chat-id") + .build(); + + @Mock private PlayerApiKeyDaoWrapper apiKeyDao; + @Mock private GameHostingApiKeyDaoWrapper gameHostingApiKeyDaoWrapper; + + @InjectMocks private ApiKeyAuthenticator authenticator; + + @Test + void keyNotFound() { + when(apiKeyDao.lookupByApiKey(API_KEY)).thenReturn(Optional.empty()); + when(gameHostingApiKeyDaoWrapper.isKeyValid(API_KEY)).thenReturn(false); + + final Optional result = authenticator.authenticate(API_KEY.getValue()); + + assertThat(result, isEmpty()); + } + + @Test + void playerKeyFound() { + when(apiKeyDao.lookupByApiKey(API_KEY)).thenReturn(Optional.of(PLAYER_DATA)); + + final Optional result = authenticator.authenticate(API_KEY.getValue()); + + assertThat( + result, + isPresentAndIs( + AuthenticatedUser.builder() + .apiKey(API_KEY) + .userId(PLAYER_DATA.getUserId()) + .name(PLAYER_DATA.getUsername()) + .userRole(PLAYER_DATA.getUserRole()) + .build())); + } + + @Test + void hostKeyFound() { + when(apiKeyDao.lookupByApiKey(API_KEY)).thenReturn(Optional.empty()); + when(gameHostingApiKeyDaoWrapper.isKeyValid(API_KEY)).thenReturn(true); + + final Optional result = authenticator.authenticate(API_KEY.getValue()); + + assertThat( + result, + isPresentAndIs( + AuthenticatedUser.builder().apiKey(API_KEY).userRole(UserRole.HOST).build())); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUserTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUserTest.java new file mode 100644 index 0000000..2b3e31e --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authentication/AuthenticatedUserTest.java @@ -0,0 +1,124 @@ +package org.triplea.spitfire.server.access.authentication; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; +import org.triplea.spitfire.server.TestData; + +/** + * We're not testing a whole lot here, the 'custom' getters of AuthenticatedUser do state checks for + * validity. In this test we are running through invalid and valid states to ensure we have the + * checks correct. + */ +@SuppressWarnings("InnerClassMayBeStatic") +class AuthenticatedUserTest { + + private static final ApiKey API_KEY = TestData.API_KEY; + private static final String NAME = "name"; + private static final int USER_ID = 10; + + private static final List rolesWithUserNames = + List.of(UserRole.ADMIN, UserRole.MODERATOR, UserRole.PLAYER, UserRole.ANONYMOUS); + + @Nested + class GetName { + @Test + void validStates() { + AuthenticatedUser.builder().userRole(UserRole.HOST).apiKey(API_KEY).build().getName(); + rolesWithUserNames.forEach( + roleWithName -> + AuthenticatedUser.builder() + .name(NAME) + .userRole(roleWithName) + .apiKey(API_KEY) + .build() + .getName()); + } + + @Test + void invalidStateIsHostWithName() { + assertThrows( + IllegalStateException.class, + () -> + AuthenticatedUser.builder() + .userRole(UserRole.HOST) + .name(NAME) + .apiKey(API_KEY) + .build() + .getName()); + } + + @Test + void invalidStateIsNonHostWithoutName() { + // following invalid because name is null + rolesWithUserNames.forEach( + roleWithName -> + assertThrows( + IllegalStateException.class, + () -> + AuthenticatedUser.builder() + .userRole(roleWithName) + .apiKey(API_KEY) + .build() + .getName())); + } + } + + @Nested + class GetUserIdOrThrow { + @Test + void validStates() { + // all named roles except anonymous and host can have a user id + AuthenticatedUser.builder() + .userId(USER_ID) + .userRole(UserRole.ADMIN) + .apiKey(API_KEY) + .build() + .getUserIdOrThrow(); + + AuthenticatedUser.builder() + .userId(USER_ID) + .userRole(UserRole.MODERATOR) + .apiKey(API_KEY) + .build() + .getUserIdOrThrow(); + + AuthenticatedUser.builder() + .userId(USER_ID) + .userRole(UserRole.PLAYER) + .apiKey(API_KEY) + .build() + .getUserIdOrThrow(); + } + + @Test + void hostWithUserIdIsInvalid() { + assertThrows( + IllegalStateException.class, + () -> + AuthenticatedUser.builder() + .userId(USER_ID) + .userRole(UserRole.HOST) + .apiKey(API_KEY) + .build() + .getUserIdOrThrow()); + } + + @Test + void anonymousWithUserIdIsInvalid() { + assertThrows( + IllegalStateException.class, + () -> + AuthenticatedUser.builder() + .userId(USER_ID) + .userRole(UserRole.ANONYMOUS) + .apiKey(API_KEY) + .build() + .getUserIdOrThrow()); + } + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterIntegrationTest.java new file mode 100644 index 0000000..672f5ca --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterIntegrationTest.java @@ -0,0 +1,56 @@ +package org.triplea.spitfire.server.access.authorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import feign.FeignException; +import java.net.URI; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.LobbyHttpClientConfig; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +/** + * In this test we'll verify that user-banned filter is configured. We'll do ban by system-id as + * that is easier to control and does not need us to ban localhost. We'll verify the ban against the + * login endpoint as that will be a common first endpoint for banned users to attempt to access + * after being booted and banned. + */ +@AllArgsConstructor +class BannedPlayerFilterIntegrationTest extends ControllerIntegrationTest { + private static final String BANNED_SYSTEM_ID = "system-id"; + + private final URI localhost; + + @Test + void banned() { + // we expect an exception because user is banned. If user were not banned + // then the login attempt would return result. + final FeignException exception = + Assertions.assertThrows( + FeignException.class, + () -> + LobbyLoginClient.newClient(localhost, headersWithSystemId(BANNED_SYSTEM_ID)) + .login("user", null)); + + assertThat(exception.status(), is(401)); + } + + @Test + void notBanned() { + // a not-banned user should be able to login with anonymouse user account + final LobbyLoginResponse lobbyLoginResponse = + LobbyLoginClient.newClient(localhost, headersWithSystemId("any other system id")) + .login("user", null); + assertThat(lobbyLoginResponse.isSuccess(), is(true)); + } + + @SuppressWarnings("SameParameterValue") + private static Map headersWithSystemId(final String systemId) { + return Map.of(LobbyHttpClientConfig.SYSTEM_ID_HEADER, systemId); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterTest.java new file mode 100644 index 0000000..9fc5ba3 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/BannedPlayerFilterTest.java @@ -0,0 +1,132 @@ +package org.triplea.spitfire.server.access.authorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.ban.BanLookupRecord; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.http.client.LobbyHttpClientConfig; + +@SuppressWarnings("SameParameterValue") +@ExtendWith(MockitoExtension.class) +class BannedPlayerFilterTest { + + private static final Instant NOW_TIME = Instant.parse("2001-01-01T23:59:59.0Z"); + private static final String BAN_ID = "public-id"; + private static final String IP = "sample-ip"; + private static final String SYSTEM_ID = "system-id"; + + @Mock private UserBanDao userBanDao; + @Mock private Clock clock; + @Mock private HttpServletRequest request; + + @InjectMocks private BannedPlayerFilter bannedPlayerFilter; + + @Mock private ContainerRequestContext containerRequestContext; + + void givenIpAndSystemId() { + when(request.getRemoteAddr()).thenReturn(IP); + when(request.getHeader(LobbyHttpClientConfig.SYSTEM_ID_HEADER)).thenReturn(SYSTEM_ID); + } + + @Nested + class NotBanned { + @Test + @DisplayName("IP and System ID are ok, verify request is *not* aborted") + void notBanned() { + givenIpAndSystemId(); + givenIpIsNotBanned(); + + bannedPlayerFilter.filter(containerRequestContext); + + verifyRequestIsAllowed(); + } + + private void givenIpIsNotBanned() { + when(userBanDao.lookupBan(IP, SYSTEM_ID)).thenReturn(Optional.empty()); + } + + private void verifyRequestIsAllowed() { + verify(containerRequestContext, never()).abortWith(any()); + } + } + + @Nested + class Banned { + @Test + @DisplayName("IP or System ID are banned, verify request is aborted") + void ipIsBanned() { + givenIpAndSystemId(); + givenCurrentTimeIs(NOW_TIME); + givenBannedWithExpiryAndBanIdentifier(NOW_TIME.plus(5, ChronoUnit.MINUTES), BAN_ID); + + bannedPlayerFilter.filter(containerRequestContext); + + verifyForbiddenRequest(); + } + + private void givenCurrentTimeIs(final Instant nowTime) { + when(clock.instant()).thenReturn(nowTime); + } + + private void givenBannedWithExpiryAndBanIdentifier( + final Instant banExpiry, final String banId) { + when(userBanDao.lookupBan(IP, SYSTEM_ID)) + .thenReturn( + Optional.of( + BanLookupRecord.builder().banExpiry(banExpiry).publicBanId(banId).build())); + } + + private void verifyForbiddenRequest() { + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(containerRequestContext).abortWith(responseCaptor.capture()); + + final Response response = responseCaptor.getValue(); + assertThat(response.getStatus(), is(Status.UNAUTHORIZED.getStatusCode())); + assertThat(response.getStatusInfo().getReasonPhrase(), containsString("5 minutes")); + assertThat(response.getStatusInfo().getReasonPhrase(), containsString(BAN_ID)); + } + } + + @Nested + class MissingSystemId { + @Test + @DisplayName("Missing system ID is a bad request and should be rejected") + void noSystemId() { + when(request.getHeader(LobbyHttpClientConfig.SYSTEM_ID_HEADER)).thenReturn(null); + + bannedPlayerFilter.filter(containerRequestContext); + + verifyForbiddenRequest(); + } + + private void verifyForbiddenRequest() { + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(containerRequestContext).abortWith(responseCaptor.capture()); + + final Response response = responseCaptor.getValue(); + assertThat(response.getStatus(), is(Status.UNAUTHORIZED.getStatusCode())); + } + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizerTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizerTest.java new file mode 100644 index 0000000..d4e2993 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/access/authorization/RoleAuthorizerTest.java @@ -0,0 +1,90 @@ +package org.triplea.spitfire.server.access.authorization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.spitfire.server.access.authentication.AuthenticatedUser; + +@ExtendWith(MockitoExtension.class) +class RoleAuthorizerTest { + + @Mock private AuthenticatedUser authenticatedUser; + + private final RoleAuthorizer roleAuthorizer = new RoleAuthorizer(); + + @Test + void authenticateAsAdmin() { + givenUserAssumingEachRoleInSequence(); + + // first iteration, user is admin + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ADMIN), is(true)); + // second iteration, user is moderator + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ADMIN), is(false)); + // third iteration, user is player + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ADMIN), is(false)); + // fourth iteration, user is anonymous + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ADMIN), is(false)); + // fifth iteration, user is host + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ADMIN), is(false)); + } + + /** Rotates user role on each authorization iteration. */ + private void givenUserAssumingEachRoleInSequence() { + when(authenticatedUser.getUserRole()) + .thenReturn(UserRole.ADMIN) + .thenReturn(UserRole.MODERATOR) + .thenReturn(UserRole.PLAYER) + .thenReturn(UserRole.ANONYMOUS) + .thenReturn(UserRole.HOST); + } + + @Test + void authenticateAsModerator() { + givenUserAssumingEachRoleInSequence(); + + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.MODERATOR), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.MODERATOR), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.MODERATOR), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.MODERATOR), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.MODERATOR), is(false)); + } + + @Test + void authenticateAsPlayer() { + givenUserAssumingEachRoleInSequence(); + + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.PLAYER), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.PLAYER), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.PLAYER), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.PLAYER), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.PLAYER), is(false)); + } + + @Test + void authenticateAsAnonymous() { + givenUserAssumingEachRoleInSequence(); + + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ANONYMOUS), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ANONYMOUS), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ANONYMOUS), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ANONYMOUS), is(true)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.ANONYMOUS), is(false)); + } + + @Test + void authenticateAsHost() { + givenUserAssumingEachRoleInSequence(); + + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.HOST), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.HOST), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.HOST), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.HOST), is(false)); + assertThat(roleAuthorizer.authorize(authenticatedUser, UserRole.HOST), is(true)); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/AccessLogControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/AccessLogControllerIntegrationTest.java new file mode 100644 index 0000000..564bc19 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/AccessLogControllerIntegrationTest.java @@ -0,0 +1,100 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.notNullValue; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.toolbox.PagingParams; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogSearchRequest; +import org.triplea.http.client.lobby.moderator.toolbox.log.ToolboxAccessLogClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class AccessLogControllerIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + private final ToolboxAccessLogClient client; + + AccessLogControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + this.client = ToolboxAccessLogClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ToolboxAccessLogClient.newClient(localhost, apiKey), + client -> + client.getAccessLog( + AccessLogSearchRequest.builder().username("username").ip("ip").build(), + PagingParams.builder().pageSize(1).rowNumber(0).build()), + client -> client.getAccessLog(PagingParams.builder().pageSize(1).rowNumber(0).build())); + } + + @Test + void getAccessLog() { + final var result = client.getAccessLog(PagingParams.builder().pageSize(1).rowNumber(0).build()); + + assertThat(result, is(not(empty()))); + assertThat(result.get(0).getAccessDate(), is(notNullValue())); + assertThat(result.get(0).getSystemId(), is(notNullValue())); + assertThat(result.get(0).getUsername(), is(notNullValue())); + } + + @Test + void getAccessLogUnauthorizedCase() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ToolboxAccessLogClient.newClient(localhost, apiKey), + client -> client.getAccessLog(PagingParams.builder().pageSize(1).rowNumber(0).build())); + } + + /** + * First we do a fetch of access log and we pick the first record. We then search by username and + * IP address using data from the first record, which should guarantee that our search will return + * at least that one result (if not more). + */ + @Test + void getAccessLogWithSearchParams() { + // first search for a user-name and IP that exist in the system + final var firstListing = + client.getAccessLog(PagingParams.builder().pageSize(1).rowNumber(0).build()).get(0); + + final var result = + client.getAccessLog( + AccessLogSearchRequest.builder() + .username(firstListing.getUsername()) + .ip(firstListing.getIp()) + .build(), + PagingParams.builder().pageSize(1).rowNumber(0).build()); + + assertThat( + "We expect there to have been at least one match for sure", result, is(not(empty()))); + assertThat( + "Username should match what we searched for", + result.get(0).getUsername(), + is(firstListing.getUsername())); + assertThat( + "IP should match what we searched for", result.get(0).getIp(), is(firstListing.getIp())); + } + + @Test + void emptySearchShouldBeSameAsAllSearch() { + // do a search with empty search parameters (should return everything) + final var emptySearchResult = + client.getAccessLog( + AccessLogSearchRequest.builder().build(), + PagingParams.builder().pageSize(1).rowNumber(0).build()); + + // do a full listing of all records + final var allSearchResult = + client.getAccessLog(PagingParams.builder().pageSize(1).rowNumber(0).build()); + + assertThat(emptySearchResult, is(equalTo(allSearchResult))); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/BadWordsControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/BadWordsControllerIntegrationTest.java new file mode 100644 index 0000000..eae4fd2 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/BadWordsControllerIntegrationTest.java @@ -0,0 +1,56 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsNot.not; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.toolbox.words.ToolboxBadWordsClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class BadWordsControllerIntegrationTest extends ControllerIntegrationTest { + + private final ToolboxBadWordsClient client; + + BadWordsControllerIntegrationTest(final URI localhost) { + this.client = ToolboxBadWordsClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @Test + void badRequests() { + assertBadRequest(() -> client.addBadWord("")); + assertBadRequest(() -> client.removeBadWord("")); + } + + @Test + void listBadWords() { + assertThat(client.getBadWords(), is(not(empty()))); + } + + @Test + void removeBadWord() { + final List badWords = client.getBadWords(); + + // remember the first entry + final String firstBadWord = badWords.get(0); + + // remove the first entry + client.removeBadWord(firstBadWord); + + // make sure entry is removed from listing + assertThat(client.getBadWords(), not(hasItem(firstBadWord))); + } + + @Test + void addBadWord() { + assertThat(client.getBadWords(), not(hasItem("bad-word-to-be-added"))); + + client.addBadWord("bad-word-to-be-added"); + + assertThat(client.getBadWords(), hasItem("bad-word-to-be-added")); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/CreateAccountControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/CreateAccountControllerIntegrationTest.java new file mode 100644 index 0000000..fc98bea --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/CreateAccountControllerIntegrationTest.java @@ -0,0 +1,56 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.login.CreateAccountResponse; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.java.Sha512Hasher; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class CreateAccountControllerIntegrationTest extends ControllerIntegrationTest { + private static final String USERNAME = "user-name"; + private static final String EMAIL = "email@email.com"; + private static final String PASSWORD = Sha512Hasher.hashPasswordWithSalt("pass"); + + private static final String USERNAME_1 = "user-name_1"; + private static final String EMAIL_1 = "email1@email.com"; + private static final String PASSWORD_1 = Sha512Hasher.hashPasswordWithSalt("pass_1"); + + private final LobbyLoginClient client; + + CreateAccountControllerIntegrationTest(final URI localhost) { + this.client = LobbyLoginClient.newClient(localhost); + } + + @Test + void badRequests() { + assertBadRequest(() -> client.login(null, null)); + assertBadRequest(() -> client.createAccount(null, null, null)); + assertBadRequest(() -> client.createAccount("user", "email@email.com", null)); + assertBadRequest(() -> client.createAccount("user", null, "password")); + assertBadRequest(() -> client.createAccount(null, "email@email.com", "password")); + } + + @Test + void createAccountAndDoLogin() { + final CreateAccountResponse result = client.createAccount(USERNAME, EMAIL, PASSWORD); + assertThat(result, is(CreateAccountResponse.SUCCESS_RESPONSE)); + + // verify login with the new account + final var loginResponse = client.login(USERNAME, PASSWORD); + assertThat(loginResponse.isSuccess(), is(true)); + } + + @Test + void duplicateAccountCreateFails() { + client.createAccount(USERNAME_1, EMAIL_1, PASSWORD_1); + + final CreateAccountResponse result = client.createAccount(USERNAME_1, EMAIL_1, PASSWORD_1); + + assertThat(result, is(not(CreateAccountResponse.SUCCESS_RESPONSE))); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/DisconnectUserControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/DisconnectUserControllerIntegrationTest.java new file mode 100644 index 0000000..f041d3e --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/DisconnectUserControllerIntegrationTest.java @@ -0,0 +1,34 @@ +package org.triplea.spitfire.server.controllers; + +import java.net.URI; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class DisconnectUserControllerIntegrationTest extends ControllerIntegrationTest { + private static final PlayerChatId CHAT_ID = PlayerChatId.of("chat-id"); + private final URI localhost; + private final ModeratorLobbyClient client; + + DisconnectUserControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + this.client = ModeratorLobbyClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ModeratorLobbyClient.newClient(localhost, apiKey), + client -> client.disconnectPlayer("chat-id")); + } + + @Test + @DisplayName("Send disconnect request, verify we get a 400 for chat-id not found") + void disconnectPlayer() { + assertBadRequest(() -> client.disconnectPlayer(CHAT_ID.getValue())); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ForgotPasswordControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ForgotPasswordControllerIntegrationTest.java new file mode 100644 index 0000000..73df643 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ForgotPasswordControllerIntegrationTest.java @@ -0,0 +1,27 @@ +package org.triplea.spitfire.server.controllers; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.forgot.password.ForgotPasswordClient; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class ForgotPasswordControllerIntegrationTest extends ControllerIntegrationTest { + private final ForgotPasswordClient client; + + ForgotPasswordControllerIntegrationTest(final URI localhost) { + this.client = ForgotPasswordClient.newClient(localhost); + } + + @Test + void badArgs() { + assertBadRequest( + () -> client.sendForgotPasswordRequest(ForgotPasswordRequest.builder().build())); + } + + @Test + void forgotPassword() { + client.sendForgotPasswordRequest( + ForgotPasswordRequest.builder().username("user").email("email").build()); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameChatHistoryControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameChatHistoryControllerIntegrationTest.java new file mode 100644 index 0000000..74b3ed1 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameChatHistoryControllerIntegrationTest.java @@ -0,0 +1,19 @@ +package org.triplea.spitfire.server.controllers; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class GameChatHistoryControllerIntegrationTest extends ControllerIntegrationTest { + private final ModeratorLobbyClient client; + + GameChatHistoryControllerIntegrationTest(final URI localhost) { + client = ModeratorLobbyClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @Test + void fetchGameChatHistory() { + client.fetchChatHistoryForGame("game-id"); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameHostingControllerTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameHostingControllerTest.java new file mode 100644 index 0000000..03d7bf1 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameHostingControllerTest.java @@ -0,0 +1,24 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.net.URI; +import org.hamcrest.core.IsNull; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.game.hosting.request.GameHostingClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class GameHostingControllerTest extends ControllerIntegrationTest { + private final GameHostingClient client; + + GameHostingControllerTest(final URI localhost) { + client = GameHostingClient.newClient(localhost); + } + + @Test + void sendGameHostingRequest() { + final var result = client.sendGameHostingRequest(); + assertThat(result, is(IsNull.notNullValue())); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingControllerTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingControllerTest.java new file mode 100644 index 0000000..6c16946 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingControllerTest.java @@ -0,0 +1,24 @@ +package org.triplea.spitfire.server.controllers; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.game.lobby.watcher.GameListingClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class GameListingControllerTest extends ControllerIntegrationTest { + private final GameListingClient client; + + GameListingControllerTest(final URI localhost) { + client = GameListingClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @Test + void fetchGames() { + client.fetchGameListing(); + } + + @Test + void bootGame() { + client.bootGame("game-id"); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingWebsocketIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingWebsocketIntegrationTest.java new file mode 100644 index 0000000..b1125b8 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/GameListingWebsocketIntegrationTest.java @@ -0,0 +1,137 @@ +package org.triplea.spitfire.server.controllers; + +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import feign.Headers; +import feign.RequestLine; +import java.net.URI; +import java.util.List; +import java.util.function.Consumer; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.LobbyGame; +import org.triplea.http.client.HttpClient; +import org.triplea.http.client.HttpConstants; +import org.triplea.http.client.lobby.AuthenticationHeaders; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingResponse; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyWatcherClient; +import org.triplea.http.client.web.socket.client.connections.PlayerToLobbyConnection; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameUpdatedMessage; +import org.triplea.spitfire.server.ControllerIntegrationTest; +import org.triplea.spitfire.server.TestData; +import org.triplea.spitfire.server.controllers.lobby.LobbyWatcherController; + +/* +GameListingWebsocketIntegrationTest > Post a game, verify listener is notified FAILED + Wanted but not invoked: + gameUpdatedListener.accept( + LobbyGameListing(gameId=690deee5-8cf7-4815-82b3-d0cc9424fa53, + lobbyGame=LobbyGame(hostAddress=127.0.0.1, hostPort=12, hostName=name, + mapName=map, playerCount=3, gameRound=1, epochMilliTimeStarted=1599358874438, + mapVersion=1, passworded=false, status=Waiting For Players, comments=comments)) + ); + -> at org.triplea.modules.game.GameListingWebsocketIntegrationTest.verifyPostGame( + GameListingWebsocketIntegrationTest.java:94) + Actually, there were zero interactions with this mock. + at org.triplea.modules.game.GameListingWebsocketIntegrationTest.verifyPostGame( + GameListingWebsocketIntegrationTest.java:94) + */ +@Disabled // Disabled due to flakiness, the above error is frequently seen and needs to be resolved. +@ExtendWith(MockitoExtension.class) +@RequiredArgsConstructor +class GameListingWebsocketIntegrationTest extends ControllerIntegrationTest { + private static final GamePostingRequest GAME_POSTING_REQUEST = + GamePostingRequest.builder().playerNames(List.of()).lobbyGame(TestData.LOBBY_GAME).build(); + + private final URI localhost; + + @Mock private Consumer gameUpdatedListener; + @Mock private Consumer gameRemovedListener; + + private LobbyWatcherClient lobbyWatcherClient; + + private GamePostingTestOverrideClient gamePostingTestOverrideClient; + + /** + * A special test-only HTTP client that can post games to lobby without a reverse connectivity + * check. This allows us to post games without actually hosting a game. + */ + @Headers({HttpConstants.CONTENT_TYPE_JSON, HttpConstants.ACCEPT_JSON}) + private interface GamePostingTestOverrideClient { + /** Posts a game, for test-only, returns a game-id from server. */ + @RequestLine("POST " + LobbyWatcherController.TEST_ONLY_GAME_POSTING_PATH) + GamePostingResponse postGame(GamePostingRequest gamePostingRequest); + } + + @BeforeEach + void setUp() { + gamePostingTestOverrideClient = + HttpClient.newClient( + GamePostingTestOverrideClient.class, + localhost, + new AuthenticationHeaders(ControllerIntegrationTest.HOST).createHeaders()); + + lobbyWatcherClient = LobbyWatcherClient.newClient(localhost, ControllerIntegrationTest.HOST); + + final var playerToLobbyConnection = + new PlayerToLobbyConnection( + localhost, + ControllerIntegrationTest.PLAYER, + error -> { + throw new AssertionError(error); + }); + playerToLobbyConnection.addMessageListener( + LobbyGameUpdatedMessage.TYPE, + messageContext -> gameUpdatedListener.accept(messageContext.getLobbyGameListing())); + playerToLobbyConnection.addMessageListener( + LobbyGameRemovedMessage.TYPE, + messageContext -> gameRemovedListener.accept(messageContext.getGameId())); + } + + @Test + @DisplayName("Post a game, verify listener is notified") + void verifyPostGame() { + final String gameId = postGame(); + + verify(gameUpdatedListener, timeout(2000L)) + .accept( + LobbyGameListing.builder() + .gameId(gameId) + .lobbyGame(GAME_POSTING_REQUEST.getLobbyGame()) + .build()); + } + + private String postGame() { + return gamePostingTestOverrideClient.postGame(GAME_POSTING_REQUEST).getGameId(); + } + + @Test + @DisplayName("Post and then remove a game, verify remove listener is notified") + void removeGame() { + final String gameId = postGame(); + lobbyWatcherClient.removeGame(gameId); + + verify(gameRemovedListener, timeout(2000L).atLeastOnce()).accept(gameId); + } + + @Test + @DisplayName("Post and then update a game, verify update listener is notified") + void gameUpdated() { + final String gameId = postGame(); + final LobbyGame updatedGame = TestData.LOBBY_GAME.withComments("new comment"); + lobbyWatcherClient.updateGame(gameId, updatedGame); + + verify(gameUpdatedListener, timeout(2000L)) + .accept(LobbyGameListing.builder().gameId(gameId).lobbyGame(updatedGame).build()); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyChatIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyChatIntegrationTest.java new file mode 100644 index 0000000..a8f84ac --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyChatIntegrationTest.java @@ -0,0 +1,257 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.IsCollectionContaining.hasItems; + +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.client.connections.PlayerToLobbyConnection; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatterListingMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerJoinedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerLeftMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateReceivedMessage; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +/** + * End-to-end test where we go through a chat sequence exercising all chat features. Runs through + * the following chat sequence: + * + *
    + *
  1. Moderator joins + *
  2. Chatter joins + *
  3. Chatter speaks + *
  4. Chatter slaps moderator + *
  5. Chatter updates their status + *
  6. Chatter leaves + *
+ * + * Chat is very listener driven, for each of the above we expect various events to be received. To + * execute this test we will create a series of lists for each event respective to each player. + * After executing each event we'll wait for the expected list to gain an event (or timeout and + * fail). Then, after receiving an event, we'll verify the event data and then continue in the + * sequence. + * + *

Of note, when a player joins they will receive two events, a 'join' event and a 'connected' + * event. The 'join' event should notify that they themselves have joined (all players receive + * this), and second, only they should receive a 'connected' event that informs them of all players + * that have joined. So we expect 'moderator' to be the only player in the connected list, when + * chatter joins we expect both moderator and chatter to be in the connected event list. + */ +@RequiredArgsConstructor +class LobbyChatIntegrationTest extends ControllerIntegrationTest { + private static final int MESSAGE_TIMEOUT = 3000; + + private static final String STATUS = "status"; + private static final String MESSAGE = "sample"; + + private static final UserName MODERATOR_NAME = UserName.of("mod"); + private static final ChatParticipant MODERATOR = + ChatParticipant.builder() + .userName(MODERATOR_NAME.getValue()) + .isModerator(true) + .status("") + .playerChatId("moderator-chat-id") + .build(); + + private static final UserName CHATTER_NAME = UserName.of("chatter"); + private static final ChatParticipant CHATTER = + ChatParticipant.builder() + .userName(CHATTER_NAME.getValue()) + .isModerator(false) + .status("") + .playerChatId("player-chat-id") + .build(); + + private final URI localhost; + + private final List modPlayerStatusEvents = new ArrayList<>(); + private final List modPlayerLeftEvents = new ArrayList<>(); + private final List modPlayerJoinedEvents = new ArrayList<>(); + private final List modPlayerSlappedEvents = new ArrayList<>(); + private final List modMessageEvents = new ArrayList<>(); + private final List modConnectedEvents = new ArrayList<>(); + private PlayerToLobbyConnection moderator; + + private final List chatterPlayerStatusEvents = + new ArrayList<>(); + private final List chatterPlayerLeftEvents = new ArrayList<>(); + private final List chatterPlayerJoinedEvents = new ArrayList<>(); + private final List chatterPlayerSlappedEvents = new ArrayList<>(); + private final List chatterMessageEvents = new ArrayList<>(); + private final List chatterConnectedEvents = new ArrayList<>(); + private PlayerToLobbyConnection chatter; + + private PlayerToLobbyConnection createModerator() { + final PlayerToLobbyConnection newModerator = + new PlayerToLobbyConnection( + localhost, + ControllerIntegrationTest.MODERATOR, + err -> { + throw new AssertionError("Error on moderator: " + err); + }); + newModerator.addMessageListener( + PlayerStatusUpdateReceivedMessage.TYPE, modPlayerStatusEvents::add); + newModerator.addMessageListener(PlayerLeftMessage.TYPE, modPlayerLeftEvents::add); + newModerator.addMessageListener(PlayerJoinedMessage.TYPE, modPlayerJoinedEvents::add); + newModerator.addMessageListener(PlayerSlapReceivedMessage.TYPE, modPlayerSlappedEvents::add); + newModerator.addMessageListener(ChatReceivedMessage.TYPE, modMessageEvents::add); + newModerator.addMessageListener(ChatterListingMessage.TYPE, modConnectedEvents::add); + return newModerator; + } + + private PlayerToLobbyConnection createChatter() { + final PlayerToLobbyConnection newChatter = + new PlayerToLobbyConnection( + localhost, + ControllerIntegrationTest.PLAYER, + err -> { + throw new AssertionError("Error on chatter: " + err); + }); + newChatter.addMessageListener( + PlayerStatusUpdateReceivedMessage.TYPE, chatterPlayerStatusEvents::add); + newChatter.addMessageListener(PlayerLeftMessage.TYPE, chatterPlayerLeftEvents::add); + newChatter.addMessageListener(PlayerJoinedMessage.TYPE, chatterPlayerJoinedEvents::add); + newChatter.addMessageListener(PlayerSlapReceivedMessage.TYPE, chatterPlayerSlappedEvents::add); + newChatter.addMessageListener(ChatReceivedMessage.TYPE, chatterMessageEvents::add); + newChatter.addMessageListener(ChatterListingMessage.TYPE, chatterConnectedEvents::add); + return newChatter; + } + + @Test + @DisplayName("Run through a chat sequence with two players exercising all chat functionality.") + void chatTest() { + moderator = createModerator(); + moderatorConnects(); + chatter = createChatter(); + chatterConnects(); + chatterChats(); + chatterSlapsMod(); + chatterUpdatesStatus(); + chatterLeaves(); + } + + private void moderatorConnects() { + moderator.sendConnectToChatMessage(); + // mod is notified that only 'mod' is in chat + verifyConnectedPlayers(modConnectedEvents, MODERATOR); + // mod should be notified of their own entry into chat + verifyPlayerJoinedEvent(modPlayerJoinedEvents, MODERATOR); + } + + private void chatterConnects() { + chatter.sendConnectToChatMessage(); + // chatter should be notified that both 'mod' and 'chatter' are in chat + verifyConnectedPlayers(chatterConnectedEvents, MODERATOR, CHATTER); + // chatter should be notified of their own entry into chat + verifyPlayerJoinedEvent(chatterPlayerJoinedEvents, CHATTER); + // wait for moderator to receive message that chatter joined + waitForMessage(modPlayerJoinedEvents, 2); + // moderator is notified that chatter has joined + assertThat( + modPlayerJoinedEvents.get(1).getChatParticipant().getUserName().getValue(), + is(CHATTER_NAME.getValue())); + assertThat(modPlayerJoinedEvents.get(1).getChatParticipant().isModerator(), is(false)); + // moderator should *not* receive a connected event when chatter joins + assertThat(modConnectedEvents, hasSize(1)); + } + + private static void verifyConnectedPlayers( + final List connectedEvents, final ChatParticipant... participants) { + waitForMessage(connectedEvents); + assertThat(connectedEvents.get(0).getChatters(), hasItems(participants)); + } + + private static void verifyPlayerJoinedEvent( + final List playerJoinedEvents, final ChatParticipant expectedPlayer) { + waitForMessage(playerJoinedEvents); + assertThat(playerJoinedEvents.get(0).getChatParticipant(), is(expectedPlayer)); + } + + private void chatterChats() { + // chatter chats + chatter.sendChatMessage(MESSAGE); + // moderator should receive chat message from chatter + verifyChatMessageEvent(modMessageEvents, new ChatReceivedMessage(CHATTER_NAME, MESSAGE)); + // chatter should receive their own chat message + verifyChatMessageEvent(chatterMessageEvents, new ChatReceivedMessage(CHATTER_NAME, MESSAGE)); + } + + private static void verifyChatMessageEvent( + final List chatMessageEvents, + final ChatReceivedMessage expectedChatMessage) { + waitForMessage(chatMessageEvents); + assertThat(chatMessageEvents.get(0), is(expectedChatMessage)); + } + + private void chatterSlapsMod() { + // chatter slaps mod + chatter.slapPlayer(MODERATOR_NAME); + // moderator is notified of the slap + waitForMessage(modPlayerSlappedEvents); + + final PlayerSlapReceivedMessage moderatorSlapped = + PlayerSlapReceivedMessage.builder() + .slappingPlayer(CHATTER_NAME.getValue()) + .slappedPlayer(MODERATOR_NAME.getValue()) + .build(); + + assertThat(modPlayerSlappedEvents.get(0), is(moderatorSlapped)); + // chatter is notified of the slap + waitForMessage(chatterPlayerSlappedEvents); + assertThat(chatterPlayerSlappedEvents.get(0), is(moderatorSlapped)); + } + + private void chatterUpdatesStatus() { + + // chatter updates their status + chatter.updateStatus(STATUS); + // moderator is notified of the status update + waitForMessage(modPlayerStatusEvents); + assertThat( + modPlayerStatusEvents.get(0), + is(new PlayerStatusUpdateReceivedMessage(CHATTER_NAME, STATUS))); + waitForMessage(chatterPlayerStatusEvents); + assertThat( + chatterPlayerStatusEvents.get(0), + is(new PlayerStatusUpdateReceivedMessage(CHATTER_NAME, STATUS))); + + // chatter is notified of their own status update + + } + + private void chatterLeaves() { + // chatter disconnects + chatter.close(); + // chatter is disconnected before they can be notified of their own disconnect + assertThat(chatterPlayerLeftEvents, empty()); + + // moderator is notified chatter has left + waitForMessage(modPlayerLeftEvents); + assertThat(modPlayerLeftEvents.get(0).getUserName(), is(CHATTER_NAME)); + } + + private static void waitForMessage(final Collection messageBuffer) { + waitForMessage(messageBuffer, 1); + } + + /** Does a busy wait loop until the given collection is at least a given size. */ + private static void waitForMessage(final Collection messageBuffer, final int minCount) { + Awaitility.await() + .atMost(Duration.ofMillis(MESSAGE_TIMEOUT)) + .until(() -> messageBuffer.size() >= minCount); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWatcherControllerTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWatcherControllerTest.java new file mode 100644 index 0000000..d8e13df --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWatcherControllerTest.java @@ -0,0 +1,88 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatUploadParams; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyWatcherClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; +import org.triplea.spitfire.server.TestData; + +class LobbyWatcherControllerTest extends ControllerIntegrationTest { + private static final GamePostingRequest GAME_POSTING_REQUEST = + GamePostingRequest.builder().playerNames(List.of()).lobbyGame(TestData.LOBBY_GAME).build(); + + private final URI localhost; + private final LobbyWatcherClient client; + + LobbyWatcherControllerTest(final URI localhost) { + this.localhost = localhost; + client = LobbyWatcherClient.newClient(localhost, ControllerIntegrationTest.HOST); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_HOST, + apiKey -> LobbyWatcherClient.newClient(localhost, apiKey), + client -> client.removeGame("game"), + client -> client.postGame(GAME_POSTING_REQUEST), + client -> client.sendKeepAlive("game-id"), + client -> + client.uploadChatMessage( + ChatUploadParams.builder() + .fromPlayer(UserName.of("player")) + .chatMessage("chat") + .gameId("game-id") + .build()), + client -> client.playerJoined("game-id", UserName.of("player-0")), + client -> client.playerLeft("game-id", UserName.of("player-0"))); + } + + @Test + void postGame() { + client.postGame(GAME_POSTING_REQUEST); + } + + @Test + void removeGame() { + client.removeGame("game-id"); + } + + @Test + void keepAlive() { + final boolean result = client.sendKeepAlive("game-id"); + assertThat(result, is(false)); + } + + @Test + void updateGame() { + client.postGame(GAME_POSTING_REQUEST); + } + + @Test + void uploadChat() { + client.uploadChatMessage( + ChatUploadParams.builder() + .fromPlayer(UserName.of("player")) + .chatMessage("chat") + .gameId("game-id") + .build()); + } + + @Test + void notifyPlayerJoined() { + client.playerJoined("game-id", UserName.of("player-0")); + } + + @Test + void notifyPlayerLeft() { + client.playerJoined("game-id", UserName.of("player-1")); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWebsocketClientIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWebsocketClientIntegrationTest.java new file mode 100644 index 0000000..f73eabc --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LobbyWebsocketClientIntegrationTest.java @@ -0,0 +1,93 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.net.URI; +import java.time.Duration; +import lombok.AllArgsConstructor; +import org.awaitility.Awaitility; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.web.socket.WebsocketPaths; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +@Disabled // Disabled until this can be made more reliable, seeing failure listed below +/* + ERROR [2020-09-06 01:52:03,515] org.triplea.web.socket.WebSocketMessagingBus: Error-id processing + websocket message, returning an error message to user. Error id: + fc66dc4b-61d8-4f87-9444-a00ba418c008 + ! java.nio.channels.ClosedChannelException: null + ! at org.eclipse.jetty.io.WriteFlusher.onClose(WriteFlusher.java:492) + ! at org.eclipse.jetty.io.AbstractEndPoint.onClose(AbstractEndPoint.java:353) + ! at org.eclipse.jetty.io.ChannelEndPoint.onClose(ChannelEndPoint.java:215) + ! at org.eclipse.jetty.io.AbstractEndPoint.doOnClose(AbstractEndPoint.java:225) + ! at org.eclipse.jetty.io.AbstractEndPoint.shutdownOutput(AbstractEndPoint.java:157) + ! at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.disconnect + (AbstractWebSocketConnection.java:327) + ! at org.eclipse.jetty.websocket.common.io.DisconnectCallback.failed(DisconnectCallback.java:36) + ! at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.close + (AbstractWebSocketConnection.java:200) + ! at org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection.onFillable + (AbstractWebSocketConnection.java:452) + ! at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305) + ! at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103) + ! at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117) + ! at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333) + ! at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310) + ! at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168) + ! at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126) + ! at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run + (ReservedThreadExecutor.java:366) + ! at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:698) + ! at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:804) + ! at java.base/java.lang.Thread.run(Thread.java:834) + INFO [2020-09-06 01:52:03,521] org.triplea.dropwizard.test.DropwizardServerExtension: + Running database cleanup.. +*/ +@AllArgsConstructor +class LobbyWebsocketClientIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + + @Test + @DisplayName("Verify basic websocket operations: open, send, close") + void verifyConnectivity(final URI host) throws Exception { + final URI websocketUri = URI.create(host + WebsocketPaths.PLAYER_CONNECTIONS); + + final WebSocketClient client = + new WebSocketClient(websocketUri) { + @Override + public void onOpen(final ServerHandshake serverHandshake) {} + + @Override + public void onMessage(final String message) {} + + @Override + public void onClose(final int code, final String reason, final boolean remote) {} + + @Override + public void onError(final Exception ex) {} + }; + + assertThat(client.isOpen(), is(false)); + client.connect(); + + Awaitility.await() + .pollDelay(Duration.ofMillis(10)) + .atMost(Duration.ofSeconds(1)) + .until(client::isOpen); + client.send("sending! Just to make sure there are no exception here."); + + // small wait to process any responses + Thread.sleep(10); + + client.close(); + Awaitility.await() + .pollDelay(Duration.ofMillis(10)) + .atMost(Duration.ofMillis(100)) + .until(() -> !client.isOpen()); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LoginControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LoginControllerIntegrationTest.java new file mode 100644 index 0000000..140406a --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/LoginControllerIntegrationTest.java @@ -0,0 +1,53 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.java.Sha512Hasher; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class LoginControllerIntegrationTest extends ControllerIntegrationTest { + private static final String USERNAME = "player"; + private static final String PASSWORD = Sha512Hasher.hashPasswordWithSalt("password"); + private static final String TEMP_PASSWORD = Sha512Hasher.hashPasswordWithSalt("temp-password"); + private static final String INVALID_PASSWORD = "invalid"; + + private final LobbyLoginClient client; + + LoginControllerIntegrationTest(final URI localhost) { + client = LobbyLoginClient.newClient(localhost); + } + + @Test + void invalidLogin() { + final LobbyLoginResponse response = client.login(USERNAME, INVALID_PASSWORD); + + assertThat(response.getFailReason(), notNullValue()); + assertThat(response.getApiKey(), nullValue()); + assertThat(response.isPasswordChangeRequired(), is(false)); + } + + @Test + void validLogin() { + final LobbyLoginResponse response = client.login(USERNAME, PASSWORD); + + assertThat(response.getFailReason(), nullValue()); + assertThat(response.getApiKey(), notNullValue()); + assertThat(response.isPasswordChangeRequired(), is(false)); + } + + @Test + void tempPasswordLogin() { + final LobbyLoginResponse response = client.login(USERNAME, TEMP_PASSWORD); + + assertThat(response.getFailReason(), nullValue()); + assertThat(response.getApiKey(), notNullValue()); + assertThat(response.isPasswordChangeRequired(), is(true)); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorAuditHistoryControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorAuditHistoryControllerIntegrationTest.java new file mode 100644 index 0000000..fe0fa40 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorAuditHistoryControllerIntegrationTest.java @@ -0,0 +1,45 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.notNullValue; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.toolbox.PagingParams; +import org.triplea.http.client.lobby.moderator.toolbox.log.ModeratorEvent; +import org.triplea.http.client.lobby.moderator.toolbox.log.ToolboxEventLogClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class ModeratorAuditHistoryControllerIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + private final ToolboxEventLogClient client; + + ModeratorAuditHistoryControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + this.client = ToolboxEventLogClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ToolboxEventLogClient.newClient(localhost, apiKey), + client -> + client.lookupModeratorEvents(PagingParams.builder().pageSize(1).rowNumber(0).build())); + } + + @Test + void fetchHistory() { + final List response = + client.lookupModeratorEvents(PagingParams.builder().pageSize(1).rowNumber(0).build()); + assertThat(response, not(empty())); + assertThat(response.get(0).getActionTarget(), notNullValue()); + assertThat(response.get(0).getModeratorAction(), notNullValue()); + assertThat(response.get(0).getDate(), notNullValue()); + assertThat(response.get(0).getModeratorName(), notNullValue()); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorsControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorsControllerIntegrationTest.java new file mode 100644 index 0000000..6dada5b --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/ModeratorsControllerIntegrationTest.java @@ -0,0 +1,60 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.toolbox.management.ToolboxModeratorManagementClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class ModeratorsControllerIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + private final ToolboxModeratorManagementClient playerClient; + private final ToolboxModeratorManagementClient moderatorClient; + private final ToolboxModeratorManagementClient adminClient; + + ModeratorsControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + this.playerClient = + ToolboxModeratorManagementClient.newClient(localhost, ControllerIntegrationTest.PLAYER); + this.moderatorClient = + ToolboxModeratorManagementClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + this.adminClient = + ToolboxModeratorManagementClient.newClient(localhost, ControllerIntegrationTest.ADMIN); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + List.of(ControllerIntegrationTest.PLAYER, ControllerIntegrationTest.MODERATOR), + apiKey -> ToolboxModeratorManagementClient.newClient(localhost, apiKey), + client -> client.addAdmin("admin"), + client -> client.addModerator("mod"), + client -> client.removeMod("mod")); + } + + @Test + void isAdmin() { + assertThat(playerClient.isCurrentUserAdmin(), is(false)); + assertThat(moderatorClient.isCurrentUserAdmin(), is(false)); + assertThat(adminClient.isCurrentUserAdmin(), is(true)); + } + + @Test + void removeMod() { + adminClient.removeMod("mod"); + } + + @Test + void addMod() { + adminClient.addModerator("mod"); + } + + @Test + void setAdmin() { + adminClient.addAdmin("admin"); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/MuteUserControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/MuteUserControllerIntegrationTest.java new file mode 100644 index 0000000..8e1ca50 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/MuteUserControllerIntegrationTest.java @@ -0,0 +1,20 @@ +package org.triplea.spitfire.server.controllers; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.ModeratorLobbyClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class MuteUserControllerIntegrationTest extends ControllerIntegrationTest { + private final ModeratorLobbyClient client; + + MuteUserControllerIntegrationTest(final URI localhost) { + this.client = ModeratorLobbyClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @Test + void muteUser() { + client.muteUser(PlayerChatId.of("chat-id"), 600); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/PlayerInfoControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/PlayerInfoControllerIntegrationTest.java new file mode 100644 index 0000000..7bc30e1 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/PlayerInfoControllerIntegrationTest.java @@ -0,0 +1,59 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; + +import java.net.URI; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.PlayerSummary; +import org.triplea.http.client.lobby.player.PlayerLobbyActionsClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class PlayerInfoControllerIntegrationTest extends ControllerIntegrationTest { + + private final PlayerLobbyActionsClient client; + private final PlayerLobbyActionsClient moderatorClient; + + PlayerInfoControllerIntegrationTest(final URI localhost) { + client = PlayerLobbyActionsClient.newClient(localhost, ControllerIntegrationTest.ANONYMOUS); + moderatorClient = + PlayerLobbyActionsClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @Test + @Disabled + /* + Disabled: non-deterministic test, needs work. + Failure message: + Reason: Bad Request + Http 400 - Bad Request: Player could not be found, have they left chat? + */ + void fetchPlayerInfo() { + final PlayerSummary playerSummary = client.fetchPlayerInformation("chatter-chat-id2"); + + assertThat(playerSummary.getCurrentGames(), is(notNullValue())); + assertThat(playerSummary.getIp(), is(nullValue())); + assertThat(playerSummary.getSystemId(), is(nullValue())); + } + + @Test + @Disabled + /* + Disabled: non-deterministic test, needs work. + Failure message: + Reason: Bad Request + Http 400 - Bad Request: Player could not be found, have they left chat? + */ + void fetchPlayerInfoAsModerator() { + final PlayerSummary playerSummary = moderatorClient.fetchPlayerInformation("chatter-chat-id2"); + + assertThat(playerSummary.getIp(), is(notNullValue())); + assertThat(playerSummary.getRegistrationDateEpochMillis(), is(notNullValue())); + assertThat(playerSummary.getAliases(), is(notNullValue())); + assertThat(playerSummary.getBans(), is(notNullValue())); + assertThat(playerSummary.getSystemId(), is(notNullValue())); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/RemoteActionsControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/RemoteActionsControllerIntegrationTest.java new file mode 100644 index 0000000..9748dd0 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/RemoteActionsControllerIntegrationTest.java @@ -0,0 +1,66 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.net.URI; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.remote.actions.RemoteActionsClient; +import org.triplea.java.IpAddressParser; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class RemoteActionsControllerIntegrationTest extends ControllerIntegrationTest { + final URI localhost; + final RemoteActionsClient client; + final RemoteActionsClient hostClient; + + RemoteActionsControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + client = RemoteActionsClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + hostClient = RemoteActionsClient.newClient(localhost, ControllerIntegrationTest.HOST); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> RemoteActionsClient.newClient(localhost, apiKey), + client -> client.sendShutdownRequest("game-id")); + + assertNotAuthorized( + ControllerIntegrationTest.NOT_HOST, + apiKey -> RemoteActionsClient.newClient(localhost, apiKey), + client -> client.checkIfPlayerIsBanned(IpAddressParser.fromString("3.3.3.3"))); + } + + @Test + void sendShutdownSignal() { + client.sendShutdownRequest("game-id"); + } + + @Test + @DisplayName("IP address is banned") + void userIsBanned() { + final boolean result = hostClient.checkIfPlayerIsBanned(IpAddressParser.fromString("1.1.1.1")); + + assertThat(result, is(true)); + } + + @Test + @DisplayName("IP address has an expired ban") + void userWasBanned() { + final boolean result = hostClient.checkIfPlayerIsBanned(IpAddressParser.fromString("1.1.1.2")); + + assertThat(result, is(false)); + } + + @Test + @DisplayName("IP address is not in ban table at all") + void userWasNeverBanned() { + final boolean result = hostClient.checkIfPlayerIsBanned(IpAddressParser.fromString("1.1.1.3")); + + assertThat(result, is(false)); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserAccountControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserAccountControllerIntegrationTest.java new file mode 100644 index 0000000..b1e2c14 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserAccountControllerIntegrationTest.java @@ -0,0 +1,52 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.notNullValue; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.user.account.UserAccountClient; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class UserAccountControllerIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + private final UserAccountClient client; + + UserAccountControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + client = UserAccountClient.newClient(localhost, ControllerIntegrationTest.PLAYER); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + List.of(ControllerIntegrationTest.ANONYMOUS), + apiKey -> UserAccountClient.newClient(localhost, apiKey), + UserAccountClient::fetchEmail, + client -> client.changeEmail("new-email"), + client -> client.changePassword("new-password")); + } + + @Test + void changePassword() { + client.changePassword("password"); + } + + @Test + void fetchEmail() { + assertThat(client.fetchEmail(), notNullValue()); + } + + @Test + void changeEmail() { + assertThat(client.fetchEmail(), is(not("email@email-test.com"))); + + client.changeEmail("email@email-test.com"); + + assertThat(client.fetchEmail().getUserEmail(), is("email@email-test.com")); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserBanControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserBanControllerIntegrationTest.java new file mode 100644 index 0000000..0a922d5 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UserBanControllerIntegrationTest.java @@ -0,0 +1,88 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.triplea.test.common.matchers.CollectionMatchers.containsMappedItem; +import static org.triplea.test.common.matchers.CollectionMatchers.doesNotContainMappedItem; + +import java.net.URI; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.ToolboxUserBanClient; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanData; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanParams; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class UserBanControllerIntegrationTest extends ControllerIntegrationTest { + + private final URI localhost; + private final ToolboxUserBanClient client; + + UserBanControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + client = ToolboxUserBanClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ToolboxUserBanClient.newClient(localhost, apiKey), + ToolboxUserBanClient::getUserBans, + client -> + client.banUser( + UserBanParams.builder() + .ip("ip") + .minutesToBan(10) + .systemId("system-id") + .username("username") + .build()), + client -> client.removeUserBan("some-username")); + } + + @Test + void listUserBans() { + assertThat(client.getUserBans(), is(not(empty()))); + } + + /** Get list of banned users. Unban the first item. */ + @Test + void removeUserNameBan() { + final UserBanData firstItem = client.getUserBans().get(0); + + assertThat( + client.getUserBans(), containsMappedItem(UserBanData::getBanId, firstItem.getBanId())); + + client.removeUserBan(firstItem.getBanId()); + + assertThat( + client.getUserBans(), + doesNotContainMappedItem(UserBanData::getBanId, firstItem.getBanId())); + } + + /** + * Generate a mostly unique user name.
+ * Ensure user name is not already banned.
+ * Add user name to banned users.
+ * Verify banned users contains the new ban.
+ */ + @Test + void addUserNameBan() { + final String userNameToBan = "user-name-to-ban-" + UUID.randomUUID().toString().substring(0, 5); + assertThat( + client.getUserBans(), doesNotContainMappedItem(UserBanData::getUsername, userNameToBan)); + + client.banUser( + UserBanParams.builder() + .username(userNameToBan) + .systemId("system-id") + .minutesToBan(10) + .ip("55.55.55.55") + .build()); + + assertThat(client.getUserBans(), containsMappedItem(UserBanData::getUsername, userNameToBan)); + } +} diff --git a/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UsernameBanControllerIntegrationTest.java b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UsernameBanControllerIntegrationTest.java new file mode 100644 index 0000000..e4cc9b6 --- /dev/null +++ b/server/dropwizard-server/src/test/java/org/triplea/spitfire/server/controllers/UsernameBanControllerIntegrationTest.java @@ -0,0 +1,93 @@ +package org.triplea.spitfire.server.controllers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsNot.not; +import static org.triplea.test.common.matchers.CollectionMatchers.containsMappedItem; +import static org.triplea.test.common.matchers.CollectionMatchers.doesNotContainMappedItem; + +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.login.LobbyLoginClient; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.http.client.lobby.moderator.toolbox.banned.name.ToolboxUsernameBanClient; +import org.triplea.http.client.lobby.moderator.toolbox.banned.name.UsernameBanData; +import org.triplea.spitfire.server.ControllerIntegrationTest; + +class UsernameBanControllerIntegrationTest extends ControllerIntegrationTest { + private final URI localhost; + private final ToolboxUsernameBanClient client; + + UsernameBanControllerIntegrationTest(final URI localhost) { + this.localhost = localhost; + this.client = + ToolboxUsernameBanClient.newClient(localhost, ControllerIntegrationTest.MODERATOR); + } + + @SuppressWarnings("unchecked") + @Test + void mustBeAuthorized() { + assertNotAuthorized( + ControllerIntegrationTest.NOT_MODERATORS, + apiKey -> ToolboxUsernameBanClient.newClient(localhost, apiKey), + ToolboxUsernameBanClient::getUsernameBans, + client -> client.addUsernameBan("some-username"), + client -> client.removeUsernameBan("some-username")); + } + + @Test + void listBans() { + final List nameBans = client.getUsernameBans(); + assertThat(nameBans, is(not(empty()))); + } + + @Test + void removeBan() { + final List nameBans = client.getUsernameBans(); + + // remember the first item + final UsernameBanData firstItem = nameBans.get(0); + + // remove the first item + client.removeUsernameBan(firstItem.getBannedName()); + + // verify first item is removed + assertThat(client.getUsernameBans(), is(not(hasItem(firstItem)))); + } + + @Test + void addBan() { + assertThat( + "Make sure bans does not contain the item we will add", + client.getUsernameBans(), + doesNotContainMappedItem( + UsernameBanData::getBannedName, "username-that-is-now-banned".toUpperCase())); + + client.addUsernameBan("username-that-is-now-banned"); + + assertThat( + "Bans should now contain the newly added item", + client.getUsernameBans(), + containsMappedItem( + UsernameBanData::getBannedName, "username-that-is-now-banned".toUpperCase())); + } + + /** + * Do a login to verify we can login. Ban the name we used for login, then repeat the login and + * verify the login is not successful. + */ + @Test + void usernameBanDisallowsLogin() { + LobbyLoginResponse loginResponse = + LobbyLoginClient.newClient(localhost).login("random-user", null); + assertThat("Verify our anonymous login worked", loginResponse.isSuccess(), is(true)); + + client.addUsernameBan("random-user"); + + loginResponse = LobbyLoginClient.newClient(localhost).login("random-user", null); + assertThat("Verify our anonymous login worked", loginResponse.isSuccess(), is(false)); + } +} diff --git a/server/dropwizard-server/src/test/resources/datasets/access_log.yml b/server/dropwizard-server/src/test/resources/datasets/access_log.yml new file mode 100644 index 0000000..ecdabdd --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/access_log.yml @@ -0,0 +1,6 @@ +access_log: + - access_time: 2010-01-01 23:59:20.0 + username: example + ip: 1.1.1.1 + system_id: system-id + lobby_user_id: null diff --git a/server/dropwizard-server/src/test/resources/datasets/bad_word.yml b/server/dropwizard-server/src/test/resources/datasets/bad_word.yml new file mode 100644 index 0000000..cedcadb --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/bad_word.yml @@ -0,0 +1,7 @@ +bad_word: + - date_created: 2010-01-01 23:59:20.0 + word: bad + - date_created: 2010-01-01 23:59:20.0 + word: awful + - date_created: 2010-01-01 23:59:20.0 + word: not nice diff --git a/server/dropwizard-server/src/test/resources/datasets/banned_user.yml b/server/dropwizard-server/src/test/resources/datasets/banned_user.yml new file mode 100644 index 0000000..53bd552 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/banned_user.yml @@ -0,0 +1,15 @@ +banned_user: + - id: 1000 + public_id: xyz + username: banned + system_id: system-id + ip: 1.1.1.1 + ban_expiry: 2100-01-01 23:59:20.0 + date_created: 2010-01-01 23:59:20.0 + - id: 1001 + public_id: xyz2 + username: banned2 + system_id: system-id2 + ip: 1.1.1.2 + ban_expiry: 2000-01-01 23:59:20.0 + date_created: 2000-01-01 23:59:20.0 diff --git a/server/dropwizard-server/src/test/resources/datasets/banned_username.yml b/server/dropwizard-server/src/test/resources/datasets/banned_username.yml new file mode 100644 index 0000000..057b65a --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/banned_username.yml @@ -0,0 +1,7 @@ +banned_username: + - date_created: 2010-01-01 23:59:20.0 + username: bad + - date_created: 2010-01-01 23:59:20.0 + username: awful + - date_created: 2010-01-01 23:59:20.0 + username: not nice diff --git a/server/dropwizard-server/src/test/resources/datasets/game_hosting_api_key.yml b/server/dropwizard-server/src/test/resources/datasets/game_hosting_api_key.yml new file mode 100644 index 0000000..39cb3e9 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/game_hosting_api_key.yml @@ -0,0 +1,6 @@ +game_hosting_api_key: + - id: 80000 + key: 06dbbb9b6ac87d97f9acca120ae4784d0eaf6865ea99788a389a384da8ab0709e77af2bfe4f4e82c6e6d375ae256aa95c2fa99ce97ce65981cfd1340257a441a + # key = sha512(HOST) + ip: "127.0.0.1" + date_created: 2010-01-01 23:59:20.0 diff --git a/server/dropwizard-server/src/test/resources/datasets/lobby_api_key.yml b/server/dropwizard-server/src/test/resources/datasets/lobby_api_key.yml new file mode 100644 index 0000000..2cca7e7 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/lobby_api_key.yml @@ -0,0 +1,33 @@ +lobby_api_key: + - key: 238b90e6e2382ddafadc35266b2fa9a371fb3962b675ccab1b5538321f469070d0f3762f29b21ac7ad772eb6bd299d09f8e75d38ed8b7067965d5d5f26ebc3f5 + # key = sha512(ADMIN) + username: "admin" + lobby_user_id: 5000 + user_role_id: 1 + player_chat_id: moderator-chat-id1 + system_id: system-id1 + ip: "127.0.0.1" + - key: b5c53a198c35646bc9d01654e353382733355a1378515507ff46e3e98e434aee97b5f24b558ca3a190eae4adbaa3c4aa3a85e6f75a93c707341e02acd810788e + # key = sha512(MODERATOR) + username: "mod" + lobby_user_id: 5001 + user_role_id: 2 + player_chat_id: chatter-chat-id2 + system_id: system-id2 + ip: "127.0.0.1" + - key: fd9e954ad9bff3693ac947a10fd3851faacf232f5f3613cbcc8fb42034db09ff7a7f344a26d27735346b396947a58e0b9950f991aeef0995f7c731274f034556 + # key = sha512(PLAYER) + username: "chatter" + lobby_user_id: 5000 + user_role_id: 3 + player_chat_id: chatter-chat-id3 + system_id: system-id3 + ip: "127.0.0.1" + - key: c989409cb5c9fd74b66ec0a6c2d2a0f1166c2f7e379794bc7511119c53388baf60e37ef0b0f8f3b854283f832fc91147b63da46eb3cef22bc394946e34943a12 + # key = sha512(ANONYMOUS) + username: "anonymous" + lobby_user_id: + user_role_id: 4 + player_chat_id: chatter-chat-id4 + system_id: system-id4 + ip: "127.0.0.1" diff --git a/server/dropwizard-server/src/test/resources/datasets/lobby_user.yml b/server/dropwizard-server/src/test/resources/datasets/lobby_user.yml new file mode 100644 index 0000000..5debfcb --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/lobby_user.yml @@ -0,0 +1,22 @@ +lobby_user: + - id: 5000 + username: "admin" + email: "test@test.com" + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + user_role_id: 1 + - id: 5001 + username: "mod" + email: "test1@test.com" + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456701_ + user_role_id: 2 + - id: 5002 + username: "mod3" + email: "test3@test.com" + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456702_ + user_role_id: 2 + - id: 5003 + username: "player" + email: "test4@test.com" + # plaintext password == password + bcrypt_password: "$2a$10$RSkV60Ky7F7.ybGmiOEUcO/ynTyUZlLqSoXSQtliSrpFf7/WEe3QO" + user_role_id: 3 diff --git a/server/dropwizard-server/src/test/resources/datasets/map_index.yml b/server/dropwizard-server/src/test/resources/datasets/map_index.yml new file mode 100644 index 0000000..cf32b32 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/map_index.yml @@ -0,0 +1,19 @@ +map_index: + - id: 10 + map_name: map-name + # Do not use "http:" in test data, DB rider logs a noisy warning about this. + # repo_url must otherwise begin with 'http' per column constraint + repo_url: 'http-map-repo-url' + description: description-repo-1 + download_url: 'http-map-repo-url/archives/master.zip' + preview_image_url: 'http-preview-image-url' + download_size_bytes: 4000 + last_commit_date: 2000-12-01 23:59:20.0 + - id: 20 + map_name: map-name-2 + repo_url: 'http-map-repo-url-2' + description: description-repo-2 + download_url: 'http-map-repo-url-2/archives/master.zip' + preview_image_url: 'http-preview-image-url-2' + download_size_bytes: 1000 + last_commit_date: 2016-01-01 23:59:20.0 diff --git a/server/dropwizard-server/src/test/resources/datasets/map_tag_value.yml b/server/dropwizard-server/src/test/resources/datasets/map_tag_value.yml new file mode 100644 index 0000000..6f1eef4 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/map_tag_value.yml @@ -0,0 +1,35 @@ +map_tag: + - id: 0 + name: Category + display_order: 1 + - id: 1 + name: Rating + display_order: 2 + +map_tag_allowed_value: + - id: 0 + map_tag_id: 0 + value: BEST + - id: 1 + map_tag_id: 0 + value: GOOD + - id: 2 + map_tag_id: 1 + value: 1 + - id: 3 + map_tag_id: 1 + value: 2 + +map_tag_value: + - id: 0 + map_tag_id: 0 + map_index_id: 10 + map_tag_allowed_value_id: 0 + - id: 1 + map_tag_id: 1 + map_index_id: 10 + map_tag_allowed_value_id: 3 + - id: 2 + map_tag_id: 0 + map_index_id: 20 + map_tag_allowed_value_id: 1 diff --git a/server/dropwizard-server/src/test/resources/datasets/moderator_action_history.yml b/server/dropwizard-server/src/test/resources/datasets/moderator_action_history.yml new file mode 100644 index 0000000..c51ca9a --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/moderator_action_history.yml @@ -0,0 +1,6 @@ +moderator_action_history: + - id: 1000 + lobby_user_id: 5000 + date_created: 2010-01-01 23:59:20.0 + action_name: example + action_target: troll diff --git a/server/dropwizard-server/src/test/resources/datasets/temp_password_request.yml b/server/dropwizard-server/src/test/resources/datasets/temp_password_request.yml new file mode 100644 index 0000000..407fcc2 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/temp_password_request.yml @@ -0,0 +1,6 @@ +temp_password_request: + - id: 1000 + lobby_user_id: 5003 + # pass == temp-password + temp_password: "$2a$10$sg3bZHeUQhx6TCI1KU0I.eJa9GLsz5LwMiyXbK9LQIq0mJc0PTIhm" + date_created: 2222-01-01 23:59:20.0 diff --git a/server/dropwizard-server/src/test/resources/datasets/user_role.yml b/server/dropwizard-server/src/test/resources/datasets/user_role.yml new file mode 100644 index 0000000..41a7f54 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/datasets/user_role.yml @@ -0,0 +1,11 @@ +user_role: + - id: 1 + name: ADMIN + - id: 2 + name: MODERATOR + - id: 3 + name: PLAYER + - id: 4 + name: ANONYMOUS + - id: 5 + name: HOST diff --git a/server/dropwizard-server/src/test/resources/db-cleanup.sql b/server/dropwizard-server/src/test/resources/db-cleanup.sql new file mode 100644 index 0000000..80cd088 --- /dev/null +++ b/server/dropwizard-server/src/test/resources/db-cleanup.sql @@ -0,0 +1,16 @@ +-- Deletes table data in proper order + +delete from access_log; +delete from bad_word; +delete from banned_user; +delete from banned_username; +delete from game_chat_history; +delete from lobby_game; +delete from game_hosting_api_key; +delete from lobby_chat_history; +delete from lobby_api_key; +delete from moderator_action_history; +delete from temp_password_request; +delete from temp_password_request_history; +delete from lobby_user; +delete from user_role; diff --git a/server/dropwizard-server/src/test/resources/dbunit.yml b/server/dropwizard-server/src/test/resources/dbunit.yml new file mode 100644 index 0000000..1f4bedc --- /dev/null +++ b/server/dropwizard-server/src/test/resources/dbunit.yml @@ -0,0 +1,5 @@ +connectionConfig: + driver: "org.postgresql.Driver" + url: "jdbc:postgresql://localhost:5432/lobby_db" + user: "lobby_user" + password: "lobby" diff --git a/server/latest-version-module/README.md b/server/latest-version-module/README.md new file mode 100644 index 0000000..99c8e01 --- /dev/null +++ b/server/latest-version-module/README.md @@ -0,0 +1,10 @@ +# Latest Version Module + +This module provides a latest engine version web-API. This would typically be +used by front-end clients to know if they are running an out of date engine version. + +The latest version data is obtained from Github's webservice API. More specifically +we query Github's API for the 'tag name' of the 'latest release'. We then keep this +data in an in-memory cache and then refresh the cache periodically. In particular +we use a cache for a faster server response, but also to limit interactions with +Github's API (it is rate limited). diff --git a/server/latest-version-module/build.gradle b/server/latest-version-module/build.gradle new file mode 100644 index 0000000..627a655 --- /dev/null +++ b/server/latest-version-module/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "java" +} + +dependencies { + implementation project(":http-clients:github-client") +// implementation project(":lib:feign-common") +// implementation project(":lib:java-extras") + implementation project(":server:server-lib") + implementation "io.dropwizard:dropwizard-core:$dropwizardVersion" +// testImplementation project(":lib:test-common") +} diff --git a/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModule.java b/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModule.java new file mode 100644 index 0000000..c9e76a7 --- /dev/null +++ b/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModule.java @@ -0,0 +1,50 @@ +package org.triplea.modules.latest.version; + +import io.dropwizard.lifecycle.Managed; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; +import lombok.Builder; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import org.triplea.http.client.github.GithubApiClient; +import org.triplea.server.lib.scheduled.tasks.ScheduledTask; + +@Slf4j +public class LatestVersionModule { + + private String latestVersion; + + public Optional getLatestVersion() { + return Optional.ofNullable(latestVersion); + } + + public void notifyLatest(final String newLatestVersion) { + log.info("Latest engine version set to: {}", newLatestVersion); + this.latestVersion = newLatestVersion; + } + + @Builder + @Value + public static class RefreshConfiguration { + Duration delay; + Duration period; + } + + public Managed buildRefreshSchedule( + final LatestVersionModuleConfig configuration, + final RefreshConfiguration refreshConfiguration) { + + final GithubApiClient githubApiClient = configuration.createGamesRepoGithubApiClient(); + + final Supplier> githubLatestVersionFetcher = + githubApiClient::fetchLatestVersion; + + return ScheduledTask.builder() + .taskName("Latest-Engine-Version-Fetcher") + .delay(refreshConfiguration.getDelay()) + .period(refreshConfiguration.getPeriod()) + .task(() -> githubLatestVersionFetcher.get().ifPresent(this::notifyLatest)) + .build(); + } +} diff --git a/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModuleConfig.java b/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModuleConfig.java new file mode 100644 index 0000000..212a3c2 --- /dev/null +++ b/server/latest-version-module/src/main/java/org/triplea/modules/latest/version/LatestVersionModuleConfig.java @@ -0,0 +1,7 @@ +package org.triplea.modules.latest.version; + +import org.triplea.http.client.github.GithubApiClient; + +public interface LatestVersionModuleConfig { + GithubApiClient createGamesRepoGithubApiClient(); +} diff --git a/server/lobby-module/README.md b/server/lobby-module/README.md new file mode 100644 index 0000000..8ee2f88 --- /dev/null +++ b/server/lobby-module/README.md @@ -0,0 +1,189 @@ +# Http-Server + +## Strategic Fit and Overview + +Http-Server is an http/REST-like backend component to complement the lobby. +The server is intended to provide a lightweight way to do client/server +interactions without needing to use the Lobby java sockets interface. + +The server is intended to be run as a stand-alone process and may access +the same database as the lobby. + +## Starting the server +Execute the main method in `ServerApplication`. If no args are provided then +defaults suitable for a development environment are used. + +### Environment Variables + +For full functionality, secrets are provided via environment variables and need +to be set prior to launching the server. By default the server should be launchable +without any additional configuration, but some elements of the system may not function +without valid values. + +## Configuration + +Application configuration is obtained from `AppConfig.java` which is wired +from a YML file that is specified at startup. + +Of note, a reference to `AppConfig` is passed to the main server application +`ServerApplication` which can then wire those properties to any endpoint +'controllers' that would need configuration values. + +## Typical Design of Endpoints + +Endpoints typically are powered by four types of classes. + +### (1) ControllerFactory class +Wires up all dependencies and creates the controller class. + +### (2) Controller class +Controller classes need to be registered in `ServerApplicaton.java` +to be enabled. + +This class contains endpoint markups and receives HTTP requests. +The controller methods should do quick/basic validation and then +delegate as much as possible to a 'service' class. + +### (3) Service class +The service class contains 'business' logic and should perform any database +interactions. The service class is also a translation layer to aggregate +and transform data from what we get database to what the front-end HTTP +client expects. + +### (4) DAO class + +These classes live in the `lobby-db` subproject, they interact +with database and should have simple input/output parameters. + +Any transaction methods can live there as a 'default' method. + +For example, password hashing should be done at the service layer +(unless we can do it by DB function in SQL directly), and then +the hashed password is passed to DAO for lookup. + +# Design Notes + +## API Keys for Moderators + +HTTP endpoints are publicly available, they can be found and attacked. +Any endpoint that initiates a moderator action will accept +headers for a moderator API key and a password for that key. + +Any such endpoints that take moderator keys should verify +the keys and lock out IP addresses that make too many attempts. + +### Key Distribution + +Super-moderators can elevate users to moderator and generate +a 'single-use-key'. This key is then provided to that moderator. + +The moderator can then 'register' the key, where they provide +the single-use-key and a password. The backend verifies +the single-use-key and then generates a new key. This is +done so that only the moderator will then 'know' the value +of their new key. The password provided is used as a salt. + +### API Key Password + +The purpose behind this is so that if an API key is compromised, +it won't be useful unless the password is also compromised too. + +This means a moderator will need to have their OS data to +be hacked and also a key logger or something that can scrape +the password from in-memory of TripleA. + +The API key password is stored in-memory when TripleA launches +and shall not be persisted anyways. + +API keys are stored in "client settings", which are stored +with the OS and persist across TripleA installations. + +## API Key Rate Limiting + +Rate-limiting: of note, the backend implementation should be careful to apply rate limiting +to any/all endpoints that take an API key so as to avoid brute-force attacks to try and crack +an API key value. + +# WARNINGS! + +## Multiple SLF4J Bindings Not Allowed + +Dropwizard uses Logback and has a binding with SLF4J baked in. Additional SLF4J bindings +should generate a warning, but will ultimately cause problems (when run from gradle) and drop +wizard may fail to start with this error: + +```bash +java.lang.IllegalStateException: Unable to acquire the logger context + at io.dropwizard.logging.LoggingUtil.getLoggerContext(LoggingUtil.java:46) +``` + +## Stream is already closed + +This can happen when the server side does not fail fast on incorrect input. +This can be if we use headers that are missing or do not check that parameters are present. + +The stack trace indicating this will look like this: +```bash +ERROR [2019-06-06 05:07:22,247] org.glassfish.jersey.server.ServerRuntime$Responder: An I/O error has occurred while writing a response message entity to the container output stream. +! java.lang.IllegalStateException: The output stream has already been closed. +``` + +The impact of this is: + - server thread hangs + - client hangs + - server does not shutdown cleanly + +This is bad as it could be used in a DDOS attack. + +### Prevention + +Essentially fail-fast: + - When looking for headers, verify headers exist or terminate the request + - Verify that all needed GET parameters are present or terminate the request + +To terminate the request, just throw a IllegalArgumentException, it'l be mapped to a 400. + +## 404 error, but endpoint is registered!? + +Make sure in addition to the `@Path` annotation on the endpoint method, +ensure the controller class has a `@Path("")` annotation on it. + +## Lobby-db-dao + +This projects contains "DAO" code, data access layer. + +Contains Java code focused on executing SQL. Parameters passed to this +layer should be as simple as possible so that this layer is as close +to pure SQL as possible. + +### JDBI result mapper + +To return data objects from JDBI queries, register mappers in `JdbiDatabase.java` + +#### Patterns for result mappers + +(1) The mapped data object should have a static `buildResultMapper` function + +(2) Use constants to reference columns names + +#### Patterns for JDBI Result Data Objects + +##### Naming + +Naming: Suffixed `DaoData.java` so it is clear that these objects +are coming from database. + +##### Do not couple front-end to the database + +Do not return DAO data objects to http clients. Typically the backend +server layer will convert DAO data objects into a shared data object +that is shared between front-end and backend. This way DB changes do +not cascade to the front-end clients. + +### Design Pattern for Transactions + + - Create a new interface; e.g.: `ModeratorKeyRegistrationDao.java` + - Add a default method with the `@Transaction` annotation. + - add a dummy select query so that JDBI sees the interface as valid + - pass the needed DAO objects as parameters to the default method + - use mockito mocks to test the method diff --git a/server/lobby-module/build.gradle b/server/lobby-module/build.gradle new file mode 100644 index 0000000..f6d019d --- /dev/null +++ b/server/lobby-module/build.gradle @@ -0,0 +1,33 @@ +plugins { + id "java" +} + +dependencies { + implementation "at.favre.lib:bcrypt:$bcryptVersion" + implementation "com.liveperson:dropwizard-websockets:$dropwizardWebsocketsVersion" + implementation "com.sun.mail:jakarta.mail:$jakartaMailVersion" + implementation "com.sun.xml.bind:jaxb-core:$jaxbCoreVersion" + implementation "com.sun.xml.bind:jaxb-impl:$jaxbImplVersion" + implementation "io.github.openfeign:feign-gson:$openFeignVersion" + implementation "javax.activation:activation:$javaxActivationVersion" + implementation "javax.xml.bind:jaxb-api:$jaxbApiVersion" + implementation "org.java-websocket:Java-WebSocket:$javaWebSocketVersion" + implementation "org.jdbi:jdbi3-core:$jdbiVersion" + implementation "org.jdbi:jdbi3-sqlobject:$jdbiVersion" +// implementation project(':game-app:domain-data') +// implementation project(':http-clients:github-client') +// implementation project(':http-clients:lobby-client') +// implementation project(':lib:feign-common') +// implementation project(':lib:java-extras') +// implementation project(':lib:websocket-client') +// implementation project(':lib:websocket-server') + testImplementation "com.github.database-rider:rider-junit5:$databaseRiderVersion" + testImplementation "com.sun.mail:jakarta.mail:$jakartaMailVersion" + testImplementation "io.dropwizard:dropwizard-testing:$dropwizardVersion" + testImplementation "org.awaitility:awaitility:$awaitilityVersion" + testImplementation "org.java-websocket:Java-WebSocket:$javaWebsocketVersion" + testImplementation "uk.co.datumedge:hamcrest-json:$hamcrestJsonVersion" +// testImplementation project(":lib:java-extras") +// testImplementation project(":lib:test-common") + testImplementation project(":server:database-test-support") +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/LobbyModuleRowMappers.java b/server/lobby-module/src/main/java/org/triplea/db/LobbyModuleRowMappers.java new file mode 100644 index 0000000..73594c6 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/LobbyModuleRowMappers.java @@ -0,0 +1,46 @@ +package org.triplea.db; + +import java.util.List; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.mapper.RowMapperFactory; +import org.jdbi.v3.core.mapper.reflect.ConstructorMapper; +import org.triplea.db.dao.access.log.AccessLogRecord; +import org.triplea.db.dao.api.key.PlayerApiKeyLookupRecord; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryRecord; +import org.triplea.db.dao.moderator.ModeratorUserDaoData; +import org.triplea.db.dao.moderator.chat.history.ChatHistoryRecord; +import org.triplea.db.dao.moderator.player.info.PlayerAliasRecord; +import org.triplea.db.dao.moderator.player.info.PlayerBanRecord; +import org.triplea.db.dao.user.ban.BanLookupRecord; +import org.triplea.db.dao.user.ban.UserBanRecord; +import org.triplea.db.dao.user.history.PlayerHistoryRecord; +import org.triplea.db.dao.user.role.UserRoleLookup; +import org.triplea.db.dao.username.ban.UsernameBanRecord; + +/** Utility to get connections to the Postgres lobby database. */ +@Slf4j +@UtilityClass +public final class LobbyModuleRowMappers { + /** + * Returns all row mappers. These are classes that map result set values to corresponding return + * objects. + */ + public static List rowMappers() { + return List.of( + ConstructorMapper.factory(AccessLogRecord.class), + ConstructorMapper.factory(BanLookupRecord.class), + ConstructorMapper.factory(ChatHistoryRecord.class), + ConstructorMapper.factory(ModeratorAuditHistoryRecord.class), + ConstructorMapper.factory(ModeratorUserDaoData.class), + ConstructorMapper.factory(PlayerAliasRecord.class), + ConstructorMapper.factory(PlayerApiKeyLookupRecord.class), + ConstructorMapper.factory(PlayerBanRecord.class), + ConstructorMapper.factory(PlayerHistoryRecord.class), + ConstructorMapper.factory(PlayerIdentifiersByApiKeyLookup.class), + ConstructorMapper.factory(UserBanRecord.class), + ConstructorMapper.factory(UsernameBanRecord.class), + ConstructorMapper.factory(UserRoleLookup.class)); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogDao.java new file mode 100644 index 0000000..ec70132 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogDao.java @@ -0,0 +1,44 @@ +package org.triplea.db.dao.access.log; + +import java.util.List; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * Provides access to the access log table. This is a table that records user data as they enter the + * lobby. Useful for statistics and for banning. + */ +public interface AccessLogDao { + + @SqlQuery( + "select" + + " access_time," + + " username," + + " ip," + + " system_id," + + " (lobby_user_id is not null) as registered" + + " from access_log" + + " where username like :username" + + " and host(ip) like :ip" + + " and system_id like :systemId" + + " order by access_time desc" + + " offset :rowOffset rows" + + " fetch next :rowCount rows only") + List fetchAccessLogRows( + @Bind("rowOffset") int rowOffset, + @Bind("rowCount") int rowCount, + @Bind("username") String username, + @Bind("ip") String ip, + @Bind("systemId") String systemId); + + @SqlUpdate( + "insert into access_log(username, ip, system_id, lobby_user_id)\n" + + "values (" + + " :username," + + " :ip::inet," + + " :systemId," + + " (select id from lobby_user where username = :username))") + int insertUserAccessRecord( + @Bind("username") String username, @Bind("ip") String ip, @Bind("systemId") String systemId); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogRecord.java new file mode 100644 index 0000000..92acb6f --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/access/log/AccessLogRecord.java @@ -0,0 +1,30 @@ +package org.triplea.db.dao.access.log; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** Return data when selecting lobby access history. */ +@Getter +public class AccessLogRecord { + private final Instant accessTime; + private final String username; + private final String ip; + private final String systemId; + private final boolean registered; + + @Builder + public AccessLogRecord( + @ColumnName("access_time") final Instant accessTime, + @ColumnName("username") final String username, + @ColumnName("ip") final String ip, + @ColumnName("system_id") final String systemId, + @ColumnName("registered") final boolean registered) { + this.accessTime = accessTime; + this.username = username; + this.ip = ip; + this.systemId = systemId; + this.registered = registered; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/ApiKeyHasher.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/ApiKeyHasher.java new file mode 100644 index 0000000..2cdc2e9 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/ApiKeyHasher.java @@ -0,0 +1,14 @@ +package org.triplea.db.dao.api.key; + +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import java.util.function.Function; +import org.triplea.domain.data.ApiKey; + +@SuppressWarnings("UnstableApiUsage") +public class ApiKeyHasher implements Function { + @Override + public String apply(final ApiKey apiKey) { + return Hashing.sha512().hashString(apiKey.getValue(), Charsets.UTF_8).toString(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDao.java new file mode 100644 index 0000000..f9275f8 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDao.java @@ -0,0 +1,13 @@ +package org.triplea.db.dao.api.key; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +interface GameHostingApiKeyDao { + @SqlUpdate("insert into game_hosting_api_key(key, ip) values(:key, :ip::inet)") + int insertKey(@Bind("key") String key, @Bind("ip") String ip); + + @SqlQuery("select exists (select * from game_hosting_api_key where key = :apiKey)") + boolean keyExists(@Bind("apiKey") String apiKey); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapper.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapper.java new file mode 100644 index 0000000..26cb309 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapper.java @@ -0,0 +1,44 @@ +package org.triplea.db.dao.api.key; + +import com.google.common.base.Preconditions; +import java.net.InetAddress; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.domain.data.ApiKey; +import org.triplea.java.Postconditions; + +/** Wrapper to abstract away DB details of how API key is stored and to provide convenience APIs. */ +@Builder +public class GameHostingApiKeyDaoWrapper { + + @Nonnull private final GameHostingApiKeyDao gameHostApiKeyDao; + @Nonnull private final Supplier keyMaker; + + /** Hashing function so that we do not store plain-text API key values in database. */ + @Nonnull private final Function keyHashingFunction; + + public static GameHostingApiKeyDaoWrapper build(final Jdbi jdbi) { + return GameHostingApiKeyDaoWrapper.builder() + .gameHostApiKeyDao(jdbi.onDemand(GameHostingApiKeyDao.class)) + .keyMaker(ApiKey::newKey) + .keyHashingFunction(new ApiKeyHasher()) + .build(); + } + + public boolean isKeyValid(final ApiKey apiKey) { + return gameHostApiKeyDao.keyExists(keyHashingFunction.apply(apiKey)); + } + + /** Creates (and stores in DB) a new API key for 'host' connections (AKA: LobbyWatcher). */ + public ApiKey newGameHostKey(final InetAddress ip) { + Preconditions.checkArgument(ip != null); + final ApiKey key = keyMaker.get(); + final int insertCount = + gameHostApiKeyDao.insertKey(keyHashingFunction.apply(key), ip.getHostAddress()); + Postconditions.assertState(insertCount == 1); + return key; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDao.java new file mode 100644 index 0000000..04a45c3 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDao.java @@ -0,0 +1,62 @@ +package org.triplea.db.dao.api.key; + +import java.util.Optional; +import javax.annotation.Nullable; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * Dao for interacting with api_key table. Api_key table stores keys that are generated on login. + * For non-anonymous accounts, the key is linked back to the players account which is used to + * determine the users 'Role'. Anonymous users are still granted API keys, they have no user id and + * given role 'ANONYMOUS'. + */ +interface PlayerApiKeyDao { + @SqlUpdate( + "insert into lobby_api_key(" + + " username, lobby_user_id, user_role_id, player_chat_id, key, system_id, ip) " + + "values " + + "(:username, :userId, :userRoleId, :playerChatId, :apiKey, :systemId, :ip::inet)") + int storeKey( + @Bind("username") String username, + @Nullable @Bind("userId") Integer userId, + @Bind("userRoleId") int userRoleId, + @Bind("playerChatId") String playerChatId, + @Bind("apiKey") String key, + @Bind("systemId") String systemId, + @Bind("ip") String ipAddress); + + @SqlQuery( + "select " + + " ak.lobby_user_id as user_id," + + " ak.id as api_key_id," + + " ak.username as username," + + " ur.name as user_role," + + " ak.player_chat_id as player_chat_id" + + " from lobby_api_key ak " + + " join user_role ur on ur.id = ak.user_role_id " + + " left join lobby_user lu on lu.id = ak.lobby_user_id " + + " where ak.key = :apiKey") + Optional lookupByApiKey(@Bind("apiKey") String apiKey); + + @SqlUpdate("delete from lobby_api_key where date_created < (now() - '7 days'::interval)") + void deleteOldKeys(); + + @SqlQuery( + "select " + + " username," + + " system_id," + + " ip" + + " from lobby_api_key " + + " where player_chat_id = :playerChatId") + Optional lookupByPlayerChatId( + @Bind("playerChatId") String playerChatId); + + @SqlQuery( + "select " + + " lobby_user_id" + + " from lobby_api_key " + + " where player_chat_id = :playerChatId") + Optional lookupPlayerIdByPlayerChatId(@Bind("playerChatId") String playerChatId); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapper.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapper.java new file mode 100644 index 0000000..5d9a1c8 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapper.java @@ -0,0 +1,111 @@ +package org.triplea.db.dao.api.key; + +import com.google.common.base.Preconditions; +import java.net.InetAddress; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.db.dao.user.role.UserRoleDao; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; +import org.triplea.java.Postconditions; + +/** Wrapper to abstract away DB details of how API key is stored and to provide convenience APIs. */ +@Builder +public class PlayerApiKeyDaoWrapper { + + @Nonnull private final PlayerApiKeyDao lobbyApiKeyDao; + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final UserRoleDao userRoleDao; + @Nonnull private final Supplier keyMaker; + + /** Hashing function so that we do not store plain-text API key values in database. */ + @Nonnull private final Function keyHashingFunction; + + public static PlayerApiKeyDaoWrapper build(final Jdbi jdbi) { + return PlayerApiKeyDaoWrapper.builder() + .lobbyApiKeyDao(jdbi.onDemand(PlayerApiKeyDao.class)) + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .userRoleDao(jdbi.onDemand(UserRoleDao.class)) + .keyMaker(ApiKey::newKey) + .keyHashingFunction(new ApiKeyHasher()) + .build(); + } + + public Optional lookupByApiKey(final ApiKey apiKey) { + return lobbyApiKeyDao.lookupByApiKey(keyHashingFunction.apply(apiKey)); + } + + /** Creates (and stores in DB) a new API key for registered or anonymous users. */ + public ApiKey newKey( + final UserName userName, + final InetAddress ip, + final SystemId systemId, + final PlayerChatId playerChatId) { + Preconditions.checkNotNull(userName); + Preconditions.checkNotNull(ip); + + final ApiKey key = keyMaker.get(); + + userJdbiDao + .lookupUserIdAndRoleIdByUserName(userName.getValue()) + .ifPresentOrElse( + userRoleLookup -> { + // insert key for registered user + insertKey( + userRoleLookup.getUserId(), + userName.getValue(), + key, + ip, + systemId, + playerChatId, + userRoleLookup.getUserRoleId()); + }, + () -> { + // insert key for anonymous user + final int anonymousUserRoleId = userRoleDao.lookupRoleId(UserRole.ANONYMOUS); + insertKey( + null, userName.getValue(), key, ip, systemId, playerChatId, anonymousUserRoleId); + }); + return key; + } + + private void insertKey( + @Nullable final Integer userId, + @Nullable final String username, + final ApiKey apiKey, + final InetAddress ipAddress, + final SystemId systemId, + final PlayerChatId playerChatId, + final int userRoleId) { + final String hashedKey = keyHashingFunction.apply(apiKey); + + final int rowsInserted = + lobbyApiKeyDao.storeKey( + username, + userId, + userRoleId, + playerChatId.getValue(), + hashedKey, + systemId.getValue(), + ipAddress.getHostAddress()); + Postconditions.assertState(rowsInserted == 1); + } + + public Optional lookupPlayerByChatId( + final PlayerChatId playerChatId) { + return lobbyApiKeyDao.lookupByPlayerChatId(playerChatId.getValue()); + } + + public Optional lookupUserIdByChatId(final PlayerChatId playerChatId) { + return lobbyApiKeyDao.lookupPlayerIdByPlayerChatId(playerChatId.getValue()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecord.java new file mode 100644 index 0000000..81e0e06 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecord.java @@ -0,0 +1,57 @@ +package org.triplea.db.dao.api.key; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jdbi.v3.core.mapper.reflect.ColumnName; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.java.Postconditions; + +/** Maps ResultSet data when querying for a users API key. */ +@Getter +@EqualsAndHashCode +@ToString +public class PlayerApiKeyLookupRecord { + private final int apiKeyId; + @Nullable private final Integer userId; + @Nonnull private final String username; + @Nonnull private final String playerChatId; + @Nonnull private final String userRole; + + @Builder(toBuilder = true) + public PlayerApiKeyLookupRecord( + @ColumnName("api_key_id") final int apiKeyId, + @Nullable @ColumnName("user_id") final Integer userId, + @ColumnName("player_chat_id") final String playerChatId, + @ColumnName("user_role") final String userRole, + @ColumnName("username") final String username) { + this.apiKeyId = apiKeyId; + this.userId = userId; + this.playerChatId = playerChatId; + this.userRole = userRole; + this.username = username; + + verifyState(); + } + + public Integer getUserId() { + return (userId == null || userId == 0) ? null : userId; + } + + private void verifyState() { + Postconditions.assertState(!userRole.equals(UserRole.HOST)); + + if (userRole.equals(UserRole.ANONYMOUS)) { + Postconditions.assertState(userId == null || userId == 0); + } else { + Postconditions.assertState( + userId != null && userId > 0, + String.format( + "Non anonymouse users must have a user id, user id: %s, user role: %s", + userId, userRole)); + } + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerIdentifiersByApiKeyLookup.java b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerIdentifiersByApiKeyLookup.java new file mode 100644 index 0000000..1db2186 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/api/key/PlayerIdentifiersByApiKeyLookup.java @@ -0,0 +1,26 @@ +package org.triplea.db.dao.api.key; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; + +@Getter +@EqualsAndHashCode +public class PlayerIdentifiersByApiKeyLookup { + private final UserName userName; + private final SystemId systemId; + private final String ip; + + @Builder + public PlayerIdentifiersByApiKeyLookup( + @ColumnName("username") final String userName, + @ColumnName("system_id") final String systemId, + @ColumnName("ip") final String ip) { + this.userName = UserName.of(userName); + this.systemId = SystemId.of(systemId); + this.ip = ip; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDao.java new file mode 100644 index 0000000..2295024 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDao.java @@ -0,0 +1,34 @@ +package org.triplea.db.dao.chat.history; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatReceivedMessage; +import org.triplea.java.StringUtils; + +/** Lobby chat history records lobby chat messages. */ +public interface LobbyChatHistoryDao { + int MESSAGE_COLUMN_LENGTH = 240; + + default void recordMessage(ChatReceivedMessage chatReceivedMessage, int apiKeyId) { + insertMessage( + chatReceivedMessage.getSender().getValue(), + apiKeyId, + StringUtils.truncate(chatReceivedMessage.getMessage(), MESSAGE_COLUMN_LENGTH)); + } + + /** + * Stores a chat message record to database. + * + * @param username The name of the user that sent the chat message. + * @param apiKeyId The ID of the API key the user used to sign in to the lobby. This is useful for + * cross-referencing to know the users IP and system-id. + * @param message The chat message contents. + */ + @SqlUpdate( + "insert into lobby_chat_history (username, lobby_api_key_id, message) " + + "values(:username, :apiKeyId, :message)") + void insertMessage( + @Bind("username") String username, + @Bind("apiKeyId") int apiKeyId, + @Bind("message") String message); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/lobby/games/LobbyGameDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/lobby/games/LobbyGameDao.java new file mode 100644 index 0000000..e7ef061 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/lobby/games/LobbyGameDao.java @@ -0,0 +1,60 @@ +package org.triplea.db.dao.lobby.games; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.triplea.db.dao.api.key.ApiKeyHasher; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatMessageUpload; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.java.Postconditions; +import org.triplea.java.StringUtils; + +/** + * Game chat history table stores chat messages that have happened in games. This data is upload by + * game servers to the lobby and is then recorded in database. + */ +public interface LobbyGameDao { + int MESSAGE_COLUMN_LENGTH = 240; + + default void insertLobbyGame(ApiKey apiKey, LobbyGameListing lobbyGameListing) { + final String hashedkey = new ApiKeyHasher().apply(apiKey); + final int insertCount = + insertLobbyGame( + lobbyGameListing.getLobbyGame().getHostName(), // + lobbyGameListing.getGameId(), + hashedkey); + Postconditions.assertState( + insertCount == 1, "Failed to insert lobby game: " + lobbyGameListing); + } + + @SqlUpdate( + "insert into lobby_game(host_name, game_id, game_hosting_api_key_id) " + + "values (" + + " :hostName," + + " :gameId," + + " (select id from game_hosting_api_key where key = :apiKey))") + int insertLobbyGame( + @Bind("hostName") String hostName, + @Bind("gameId") String gameId, + @Bind("apiKey") String apiKey); + + default void recordChat(final ChatMessageUpload chatMessageUpload) { + final int rowInsert = + insertChatMessage( + chatMessageUpload.getGameId(), + chatMessageUpload.getFromPlayer(), + StringUtils.truncate(chatMessageUpload.getChatMessage(), MESSAGE_COLUMN_LENGTH)); + Postconditions.assertState(rowInsert == 1, "Failed to insert message: " + chatMessageUpload); + } + + @SqlUpdate( + "insert into game_chat_history (lobby_game_id, username, message) " + + "values(" + + " (select id from lobby_game where game_id = :gameId)," + + ":username, " + + ":message)") + int insertChatMessage( + @Bind("gameId") String gameId, + @Bind("username") String username, + @Bind("message") String message); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/BadWordsDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/BadWordsDao.java new file mode 100644 index 0000000..d7308a3 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/BadWordsDao.java @@ -0,0 +1,23 @@ +package org.triplea.db.dao.moderator; + +import java.util.List; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** DAO interface for interacting with the badword table. Essentially provides CRUD operations. */ +public interface BadWordsDao { + + @SqlQuery("select word from bad_word order by word") + List getBadWords(); + + @SqlUpdate("insert into bad_word (word) values (:word)") + int addBadWord(@Bind("word") String badWordToAdd); + + @SqlUpdate("delete from bad_word where word = :word") + int removeBadWord(@Bind("word") String badWordToRemove); + + @SqlQuery( + "select exists (select * from bad_word where lower(:word) like '%' || lower(word) || '%')") + boolean containsBadWord(@Bind("word") String word); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDao.java new file mode 100644 index 0000000..972d2e0 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDao.java @@ -0,0 +1,80 @@ +package org.triplea.db.dao.moderator; + +import com.google.common.base.Preconditions; +import java.util.List; +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * Interface for adding new moderator audit records to database. These records keep track of which + * actions moderators have taken, who the target was and which moderator took the action. + */ +public interface ModeratorAuditHistoryDao { + /** The set of moderator actions. */ + enum AuditAction { + BAN_MAC, + BAN_USERNAME, + REMOVE_USERNAME_BAN, + BOOT_GAME, + BOOT_USER_FROM_BOT, + BOOT_USER_FROM_LOBBY, + BAN_PLAYER_FROM_BOT, + ADD_BAD_WORD, + REMOVE_BAD_WORD, + BAN_USER, + REMOVE_USER_BAN, + ADD_MODERATOR, + REMOVE_MODERATOR, + ADD_SUPER_MOD, + DISCONNECT_USER, + DISCONNECT_GAME, + REMOTE_SHUTDOWN, + } + + /** Parameters needed when adding an audit record. */ + @Getter + @Builder + @ToString + @EqualsAndHashCode + final class AuditArgs { + @Nonnull private final Integer moderatorUserId; + @Nonnull private final AuditAction actionName; + @Nonnull private final String actionTarget; + } + + default void addAuditRecord(AuditArgs auditArgs) { + final int rowsInserted = + insertAuditRecord( + auditArgs.moderatorUserId, auditArgs.actionName.toString(), auditArgs.actionTarget); + Preconditions.checkState(rowsInserted == 1); + } + + @SqlUpdate( + "insert into moderator_action_history " + + " (lobby_user_id, action_name, action_target) " + + "values (:moderatorId, :actionName, :actionTarget)") + int insertAuditRecord( + @Bind("moderatorId") int moderatorId, + @Bind("actionName") String actionName, + @Bind("actionTarget") String actionTarget); + + @SqlQuery( + "select" + + " h.date_created," + + " u.username," + + " h.action_name," + + " h.action_target" + + " from moderator_action_history h" + + " join lobby_user u on u.id = h.lobby_user_id" + + " order by h.date_created desc" + + " offset :rowOffset rows" + + " fetch next :rowCount rows only") + List lookupHistoryItems( + @Bind("rowOffset") int rowOffset, @Bind("rowCount") int rowCount); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryRecord.java new file mode 100644 index 0000000..72a7a40 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryRecord.java @@ -0,0 +1,27 @@ +package org.triplea.db.dao.moderator; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** Return data when selecting moderator audit history. */ +@Getter +public class ModeratorAuditHistoryRecord { + private final Instant dateCreated; + private final String username; + private final String actionName; + private final String actionTarget; + + @Builder + public ModeratorAuditHistoryRecord( + @ColumnName("date_created") final Instant dateCreated, + @ColumnName("username") final String username, + @ColumnName("action_name") final String actionName, + @ColumnName("action_target") final String actionTarget) { + this.dateCreated = dateCreated; + this.username = username; + this.actionName = actionName; + this.actionTarget = actionTarget; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorUserDaoData.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorUserDaoData.java new file mode 100644 index 0000000..b165c8b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorUserDaoData.java @@ -0,0 +1,21 @@ +package org.triplea.db.dao.moderator; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** Return data when querying the user table for moderators. */ +@Getter +public class ModeratorUserDaoData { + private final String username; + private final Instant lastLogin; + + @Builder + public ModeratorUserDaoData( + @ColumnName("username") final String username, + @ColumnName("access_time") final Instant lastLogin) { + this.username = username; + this.lastLogin = lastLogin; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorsDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorsDao.java new file mode 100644 index 0000000..e6608cc --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/ModeratorsDao.java @@ -0,0 +1,35 @@ +package org.triplea.db.dao.moderator; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.BindList; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.triplea.db.dao.user.role.UserRole; + +/** DAO for managing moderator users. */ +public interface ModeratorsDao { + @SqlQuery( + "select " + + " lu.username," + + " max(al.access_time) access_time" + + " from lobby_user lu" + + " left join access_log al on al.lobby_user_id = lu.id" + + " join user_role ur on ur.id = lu.user_role_id" + + " where ur.name in ()" + + " group by lu.username" + + " order by lu.username") + List getUserByRole(@BindList("roles") Collection roles); + + default List getModerators() { + return getUserByRole(Set.of(UserRole.MODERATOR, UserRole.ADMIN)); + } + + @SqlUpdate( + "update lobby_user" + + " set user_role_id = (select id from user_role where name = :role)" + + " where id = :userId") + int setRole(@Bind("userId") int userId, @Bind("role") String role); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecord.java new file mode 100644 index 0000000..466fb78 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecord.java @@ -0,0 +1,34 @@ +package org.triplea.db.dao.moderator.chat.history; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; +import org.triplea.http.client.lobby.moderator.ChatHistoryMessage; + +/** Represents most of a row of the game_chat_history table, who said what and when. */ +@Getter(onMethod_ = @VisibleForTesting) +public class ChatHistoryRecord { + private final Instant date; + private final String username; + private final String message; + + @Builder + public ChatHistoryRecord( + @ColumnName("date") final Instant date, + @ColumnName("username") final String username, + @ColumnName("message") final String message) { + this.date = date; + this.username = username; + this.message = message; + } + + public ChatHistoryMessage toChatHistoryMessage() { + return ChatHistoryMessage.builder() + .epochMilliDate(date.toEpochMilli()) + .username(username) + .message(message) + .build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDao.java new file mode 100644 index 0000000..8727100 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDao.java @@ -0,0 +1,24 @@ +package org.triplea.db.dao.moderator.chat.history; + +import java.util.List; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; + +/** + * DAO to access history of in-game chat messages stored in database. Chat messages are stored for + * all lobby-connected games. + */ +public interface GameChatHistoryDao { + + @SqlQuery( + "select " + + " gch.date," + + " gch.username," + + " gch.message" + + " from game_chat_history gch" + + " join lobby_game lg on lg.id = gch.lobby_game_id" + + " where" + + " lg.game_id = :gameId" + + " and date > (now() - '6 hour'::interval)") + List getChatHistory(@Bind("gameId") String gameId); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecord.java new file mode 100644 index 0000000..c8da08d --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecord.java @@ -0,0 +1,41 @@ +package org.triplea.db.dao.moderator.player.info; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; +import org.triplea.http.client.lobby.moderator.PlayerSummary.Alias; + +/** + * Represents all distinct matching rows (within the last N days) in the access log table with a + * matching IP or system ID. This should tell us each name, or aliases, that was presumably used by + * a given player. + */ +@Getter +public class PlayerAliasRecord { + private final String username; + private final String ip; + private final String systemId; + private final Instant date; + + @Builder + public PlayerAliasRecord( + @ColumnName("name") final String username, + @ColumnName("ip") final String ip, + @ColumnName("systemId") final String systemId, + @ColumnName("accessTime") final Instant accessTime) { + this.username = username; + this.ip = ip; + this.systemId = systemId; + this.date = accessTime; + } + + public Alias toAlias() { + return Alias.builder() + .name(username) + .ip(ip) + .systemId(systemId) + .epochMilliDate(date.toEpochMilli()) + .build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecord.java new file mode 100644 index 0000000..e67d39a --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecord.java @@ -0,0 +1,44 @@ +package org.triplea.db.dao.moderator.player.info; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; +import org.triplea.http.client.lobby.moderator.PlayerSummary.BanInformation; + +/** + * Represents each row from the ban table where a given system id or IP was banned (within the last + * N days). + */ +@Getter +public class PlayerBanRecord { + private final String username; + private final String ip; + private final String systemId; + private final Instant banStart; + private final Instant banEnd; + + @Builder + public PlayerBanRecord( + @ColumnName("username") final String username, + @ColumnName("ip") final String ip, + @ColumnName("system_id") final String systemId, + @ColumnName("date_created") final Instant banStart, + @ColumnName("ban_expiry") final Instant banEnd) { + this.username = username; + this.ip = ip; + this.systemId = systemId; + this.banStart = banStart; + this.banEnd = banEnd; + } + + public BanInformation toBanInformation() { + return BanInformation.builder() + .name(username) + .ip(ip) + .systemId(systemId) + .epochMilliStartDate(banStart.toEpochMilli()) + .epochMillEndDate(banEnd.toEpochMilli()) + .build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDao.java new file mode 100644 index 0000000..94baa20 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDao.java @@ -0,0 +1,48 @@ +package org.triplea.db.dao.moderator.player.info; + +import java.util.List; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; + +/** + * DAO used to lookup player correlation information for moderators. Answers questions such as + * + *

    + *
  • "how many times and was this player banned?" + *
  • "which other names were used by this same IP and system-id? (presumably the same player)" + *
+ */ +public interface PlayerInfoForModeratorDao { + @SqlQuery( + "select distinct" + + " username as name," + + " ip as ip," + + " system_id as systemId," + + " max(access_time) as accessTime" + + " from access_log" + + " where " + + " access_time > (now() - '14 day'::interval)" + + " and (" + + " ip = :ip::inet" + + " or system_id = :systemId" + + " )" + + " group by name, ip, systemId" + + " order by accessTime desc") + List lookupPlayerAliasRecords( + @Bind("systemId") String systemId, @Bind("ip") String ip); + + @SqlQuery( + "select" + + " username," + + " ip," + + " system_id," + + " date_created," + + " ban_expiry" + + " from banned_user" + + " where " + + " ip = :ip::inet" + + " or system_id = :systemId" + + " order by ban_expiry desc") + List lookupPlayerBanRecords( + @Bind("systemId") String systemId, @Bind("ip") String ip); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordDao.java new file mode 100644 index 0000000..93bc0f4 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordDao.java @@ -0,0 +1,59 @@ +package org.triplea.db.dao.temp.password; + +import java.util.Optional; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.jdbi.v3.sqlobject.transaction.Transaction; + +/** + * DAO for CRUD operations on temp password table. A table that stores temporary passwords issued to + * them with the 'forgot password' feature. + */ +public interface TempPasswordDao { + String TEMP_PASSWORD_EXPIRATION = "1 day"; + + @SqlQuery( + "select temp_password" + + " from temp_password_request t" + + " join lobby_user lu on lu.id = t.lobby_user_id" + + " where lu.username = :username" + + " and t.date_created > (now() - '" + + TEMP_PASSWORD_EXPIRATION + + "'::interval)" + + " and t.date_invalidated is null") + Optional fetchTempPassword(@Bind("username") String username); + + @SqlQuery("select id from lobby_user where username = :username") + Optional lookupUserIdByUsername(@Bind("username") String username); + + @SqlUpdate( + "insert into temp_password_request" + + " (lobby_user_id, temp_password)" + + " values (:userId, :password)") + void insertPassword(@Bind("userId") int userId, @Bind("password") String password); + + @SqlQuery("select id from lobby_user where username = :username and email = :email") + Optional lookupUserIdByUsernameAndEmail( + @Bind("username") String username, @Bind("email") String email); + + @SqlUpdate( + "update temp_password_request" + + " set date_invalidated = now()" + + " where lobby_user_id = (select id from lobby_user where username = :playerName)" + + " and date_invalidated is null") + int invalidateTempPasswords(@Bind("playerName") String playerName); + + @Transaction + default boolean insertTempPassword( + final String username, final String email, final String password) { + return lookupUserIdByUsernameAndEmail(username, email) + .map( + userId -> { + invalidateTempPasswords(username); + insertPassword(userId, password); + return true; + }) + .orElse(false); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDao.java new file mode 100644 index 0000000..6835479 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDao.java @@ -0,0 +1,31 @@ +package org.triplea.db.dao.temp.password; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * DAO for CRUD operations on temp password history table. A table that stores a history of requests + * for temporary passwords. + */ +public interface TempPasswordHistoryDao { + + /** + * Returns the number of temp password requests made in the last day from a particular IP address. + */ + @SqlQuery( + "select count(*)" + + " from temp_password_request_history" + + " where inetaddress = :inetAddress::inet" + + " and date_created > (now() - '1 day'::interval)") + int countRequestsFromAddress(@Bind("inetAddress") String address); + + /** + * Records a temp password request being made from a given IP address and for a given username. + */ + @SqlUpdate( + "insert into temp_password_request_history(inetaddress, username)" + + " values(:inetaddress::inet, :username)") + void recordTempPasswordRequest( + @Bind("inetaddress") String address, @Bind("username") String username); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/UserJdbiDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/UserJdbiDao.java new file mode 100644 index 0000000..8ff0a54 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/UserJdbiDao.java @@ -0,0 +1,51 @@ +package org.triplea.db.dao.user; + +import java.util.Optional; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.db.dao.user.role.UserRoleLookup; + +/** Data access object for the users table. */ +public interface UserJdbiDao { + @SqlQuery("select id from lobby_user where username = :username") + Optional lookupUserIdByName(@Bind("username") String username); + + @SqlQuery("select bcrypt_password from lobby_user where username = :username") + Optional getPassword(@Bind("username") String username); + + @SqlUpdate("update lobby_user set bcrypt_password = :newPassword where id = :userId") + int updatePassword(@Bind("userId") int userId, @Bind("newPassword") String newPassword); + + @SqlQuery("select email from lobby_user where id = :userId") + String fetchEmail(@Bind("userId") int userId); + + @SqlUpdate("update lobby_user set email = :newEmail where id = :userId") + int updateEmail(@Bind("userId") int userId, @Bind("newEmail") String newEmail); + + @SqlQuery( + "select " + + " id," + + " user_role_id" + + " from lobby_user" + + " where username = :username") + Optional lookupUserIdAndRoleIdByUserName(@Bind("username") String username); + + @SqlQuery( + "select ur.name from user_role ur " + + "join lobby_user lu on lu.user_role_id = ur.id " + + "where lu.username = :username") + Optional lookupUserRoleByUserName(@Bind("username") String username); + + @SqlUpdate( + "insert into lobby_user(username, email, bcrypt_password, user_role_id) " + + "select " + + ":username, :email, :password, (select id from user_role where name = '" + + UserRole.PLAYER + + "') as role_id") + int createUser( + @Bind("username") String username, + @Bind("email") String email, + @Bind("password") String password); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/BanLookupRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/BanLookupRecord.java new file mode 100644 index 0000000..37329fa --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/BanLookupRecord.java @@ -0,0 +1,27 @@ +package org.triplea.db.dao.user.ban; + +import java.time.Instant; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** + * Lookup record to determine if a player is banned. If so, gives enough information to inform the + * player of the ban duration and the 'public id' of the ban. + */ +@Getter +@EqualsAndHashCode +public class BanLookupRecord { + + private final String publicBanId; + private final Instant banExpiry; + + @Builder + public BanLookupRecord( + @ColumnName("public_id") final String publicBanId, + @ColumnName("ban_expiry") final Instant banExpiry) { + this.publicBanId = publicBanId; + this.banExpiry = banExpiry; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanDao.java new file mode 100644 index 0000000..2a4b35e --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanDao.java @@ -0,0 +1,62 @@ +package org.triplea.db.dao.user.ban; + +import java.util.List; +import java.util.Optional; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** DAO for managing user bans (CRUD operations). */ +public interface UserBanDao { + @SqlQuery( + "select " + + " public_id," + + " username," + + " system_id," + + " ip," + + " ban_expiry," + + " date_created" + + " from banned_user" + + " where ban_expiry > now()" + + " order by date_created desc") + List lookupBans(); + + @SqlQuery( + "select " + + " public_id," + + " ban_expiry" + + " from banned_user" + + " where (ip = :ip::inet or system_id = :systemId)" + + " and ban_expiry > now()" + + " order by ban_expiry desc " + + " limit 1") + Optional lookupBan(@Bind("ip") String ip, @Bind("systemId") String systemId); + + @SqlQuery( + "select exists (" + + " select * " + + " from banned_user " + + " where ip = :ip::inet and ban_expiry > now()" + + ")") + boolean isBannedByIp(@Bind("ip") String ip); + + @SqlQuery( + "select username" // + + " from banned_user" + + " where public_id = :banId") + Optional lookupUsernameByBanId(@Bind("banId") String banId); + + @SqlUpdate("delete from banned_user where public_id = :banId") + int removeBan(@Bind("banId") String banId); + + @SqlUpdate( + "insert into banned_user" + + "(public_id, username, system_id, ip, ban_expiry) values\n" + + "(:banId, :username, :systemId, :ip::inet, now() + :banMinutes * '1 minute'::interval)") + int addBan( + @Bind("banId") String banId, + @Bind("username") String username, + @Bind("systemId") String systemId, + @Bind("ip") String ip, + @Bind("banMinutes") long banMinutes); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanRecord.java new file mode 100644 index 0000000..8db7af1 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/ban/UserBanRecord.java @@ -0,0 +1,38 @@ +package org.triplea.db.dao.user.ban; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** + * Return data when querying about user bans. The public ban id is for public reference, this is a + * value we can show to banned users so they can report which ban is impacting them. With that + * information we have the ability to remove the ban without needing to ask them for mac or IP. + */ +@Getter +public class UserBanRecord { + + private final String publicBanId; + private final String username; + private final String systemId; + private final String ip; + private final Instant banExpiry; + private final Instant dateCreated; + + @Builder + public UserBanRecord( + @ColumnName("public_id") final String publicBanId, + @ColumnName("username") final String username, + @ColumnName("system_id") final String systemId, + @ColumnName("ip") final String ip, + @ColumnName("ban_expiry") final Instant banExpiry, + @ColumnName("date_created") final Instant dateCreated) { + this.publicBanId = publicBanId; + this.username = username; + this.systemId = systemId; + this.ip = ip; + this.banExpiry = banExpiry; + this.dateCreated = dateCreated; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryDao.java new file mode 100644 index 0000000..7bc561f --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryDao.java @@ -0,0 +1,16 @@ +package org.triplea.db.dao.user.history; + +import java.util.Optional; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; + +/** DAO to look up a players stats. Intended to be used when getting player information. */ +public interface PlayerHistoryDao { + + @SqlQuery( + "select" + + " date_created as date_registered" + + " from lobby_user" + + " where id = :userId") + Optional lookupPlayerHistoryByUserId(@Bind("userId") int userId); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryRecord.java new file mode 100644 index 0000000..6207cff --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/history/PlayerHistoryRecord.java @@ -0,0 +1,15 @@ +package org.triplea.db.dao.user.history; + +import java.time.Instant; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +@Getter +public class PlayerHistoryRecord { + + private final long registrationDate; + + public PlayerHistoryRecord(@ColumnName("date_registered") final Instant registrationDate) { + this.registrationDate = registrationDate.toEpochMilli(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRole.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRole.java new file mode 100644 index 0000000..d62fad2 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRole.java @@ -0,0 +1,20 @@ +package org.triplea.db.dao.user.role; + +import lombok.experimental.UtilityClass; + +/** + * Class to hold constants representing the possible set of user role values.
+ * Note: this is not an enum to allow these values to be referenced from annotations. + */ +@UtilityClass +public final class UserRole { + public static final String ADMIN = "ADMIN"; + public static final String MODERATOR = "MODERATOR"; + public static final String PLAYER = "PLAYER"; + public static final String ANONYMOUS = "ANONYMOUS"; + public static final String HOST = "HOST"; + + public static boolean isModerator(final String roleName) { + return ADMIN.equals(roleName) || MODERATOR.equals(roleName); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleDao.java new file mode 100644 index 0000000..eaefd1f --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleDao.java @@ -0,0 +1,9 @@ +package org.triplea.db.dao.user.role; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; + +public interface UserRoleDao { + @SqlQuery("select id from user_role where name = :userRole") + int lookupRoleId(@Bind("userRole") String userRole); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleLookup.java b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleLookup.java new file mode 100644 index 0000000..d1a5ad7 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/user/role/UserRoleLookup.java @@ -0,0 +1,20 @@ +package org.triplea.db.dao.user.role; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +@Getter +@EqualsAndHashCode +public class UserRoleLookup { + private final int userId; + private final int userRoleId; + + @Builder + public UserRoleLookup( + @ColumnName("id") final int userId, @ColumnName("user_role_id") final int userRoleId) { + this.userId = userId; + this.userRoleId = userRoleId; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanDao.java b/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanDao.java new file mode 100644 index 0000000..33c9abe --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanDao.java @@ -0,0 +1,34 @@ +package org.triplea.db.dao.username.ban; + +import java.util.List; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** Interface with the banned_names table, these are exact match names not allowed in the lobby. */ +public interface UsernameBanDao { + + @SqlQuery( + "select" + + " username," + + " date_created" + + " from banned_username" + + " order by username asc") + List getBannedUserNames(); + + @SqlUpdate( + "insert into banned_username(username)\n" + + "values(:nameToBan)\n" + + "on conflict(username) do nothing") + int addBannedUserName(@Bind("nameToBan") String nameToBan); + + @SqlUpdate("delete from banned_username where username = :nameToRemove") + int removeBannedUserName(@Bind("nameToRemove") String nameToRemove); + + @SqlQuery( + "select exists ( " + + "select * " + + "from banned_username " + + "where username = upper(:playerName))") + boolean nameIsBanned(@Bind("playerName") String playerName); +} diff --git a/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanRecord.java b/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanRecord.java new file mode 100644 index 0000000..ffa2c0a --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/db/dao/username/ban/UsernameBanRecord.java @@ -0,0 +1,21 @@ +package org.triplea.db.dao.username.ban; + +import java.time.Instant; +import lombok.Builder; +import lombok.Getter; +import org.jdbi.v3.core.mapper.reflect.ColumnName; + +/** Return data when querying the banned username table. */ +@Getter +public class UsernameBanRecord { + private final String username; + private final Instant dateCreated; + + @Builder + public UsernameBanRecord( + @ColumnName("username") final String username, + @ColumnName("date_created") final Instant dateCreated) { + this.username = username; + this.dateCreated = dateCreated; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/LobbyModuleConfig.java b/server/lobby-module/src/main/java/org/triplea/modules/LobbyModuleConfig.java new file mode 100644 index 0000000..5abd6db --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/LobbyModuleConfig.java @@ -0,0 +1,18 @@ +package org.triplea.modules; + +import org.triplea.http.client.github.GithubApiClient; + +/** + * Interface for configuration parameters, typically these values will come from the servers + * 'configuration.yml' file. + */ +public interface LobbyModuleConfig { + /** + * When true we will do a reverse connectivity check to game hosts. Otherwise false will enable a + * special test only endpoint that still does authentication but will not do the reverse + * connectivity check. + */ + boolean isGameHostConnectivityCheckEnabled(); + + GithubApiClient createGamesRepoGithubApiClient(); +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatMessagingService.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatMessagingService.java new file mode 100644 index 0000000..95d0d6b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatMessagingService.java @@ -0,0 +1,45 @@ +package org.triplea.modules.chat; + +import com.google.common.base.Preconditions; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatSentMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ConnectToChatMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapSentMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateSentMessage; +import org.triplea.modules.chat.event.processing.ChatMessageListener; +import org.triplea.modules.chat.event.processing.PlayerConnectedListener; +import org.triplea.modules.chat.event.processing.PlayerLeftListener; +import org.triplea.modules.chat.event.processing.SlapListener; +import org.triplea.modules.chat.event.processing.StatusUpdateListener; +import org.triplea.web.socket.WebSocketMessagingBus; + +@Builder +public class ChatMessagingService { + private final PlayerConnectedListener playerConnectedListener; + private final ChatMessageListener chatMessageListener; + private final StatusUpdateListener statusUpdateListener; + private final SlapListener slapListener; + private final PlayerLeftListener playerLeftListener; + + public static ChatMessagingService build(final Chatters chatters, final Jdbi jdbi) { + Preconditions.checkNotNull(chatters); + return ChatMessagingService.builder() + .playerConnectedListener(PlayerConnectedListener.build(chatters, jdbi)) + .chatMessageListener(ChatMessageListener.build(chatters, jdbi)) + .statusUpdateListener(new StatusUpdateListener(chatters)) + .slapListener(new SlapListener(chatters)) + .playerLeftListener(new PlayerLeftListener(chatters)) + .build(); + } + + public void configure(final WebSocketMessagingBus playerConnectionMessagingBus) { + playerConnectionMessagingBus.addMessageListener( + ConnectToChatMessage.TYPE, playerConnectedListener); + playerConnectionMessagingBus.addMessageListener(ChatSentMessage.TYPE, chatMessageListener); + playerConnectionMessagingBus.addMessageListener(PlayerSlapSentMessage.TYPE, slapListener); + playerConnectionMessagingBus.addMessageListener( + PlayerStatusUpdateSentMessage.TYPE, statusUpdateListener); + playerConnectionMessagingBus.addSessionDisconnectListener(playerLeftListener); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatterSession.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatterSession.java new file mode 100644 index 0000000..74ab930 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/ChatterSession.java @@ -0,0 +1,18 @@ +package org.triplea.modules.chat; + +import java.net.InetAddress; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.web.socket.WebSocketSession; + +@Getter +@Builder +@EqualsAndHashCode +public class ChatterSession { + private final ChatParticipant chatParticipant; + private final WebSocketSession session; + private final int apiKeyId; + private final InetAddress ip; +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/Chatters.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/Chatters.java new file mode 100644 index 0000000..e2ee047 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/Chatters.java @@ -0,0 +1,185 @@ +package org.triplea.modules.chat; + +import com.google.common.annotations.VisibleForTesting; +import java.net.InetAddress; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javax.websocket.CloseReason; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.web.socket.MessageBroadcaster; +import org.triplea.web.socket.WebSocketSession; + +/** Keeps the current list of ChatParticipants and maps them to their websocket session. */ +@AllArgsConstructor +public class Chatters { + @Getter(value = AccessLevel.PACKAGE, onMethod_ = @VisibleForTesting) + private final Map participants = new ConcurrentHashMap<>(); + + private final Map playerMutes = new HashMap<>(); + + public static Chatters build() { + return new Chatters(); + } + + public Optional lookupPlayerBySession(final WebSocketSession senderSession) { + return Optional.ofNullable(participants.get(senderSession.getId())); + } + + public Optional lookupPlayerByChatId(final PlayerChatId playerChatId) { + return participants.values().stream() + .filter( + chatterSession -> + chatterSession.getChatParticipant().getPlayerChatId().equals(playerChatId)) + .findAny(); + } + + public void connectPlayer(final ChatterSession chatterSession) { + participants.put(chatterSession.getSession().getId(), chatterSession); + } + + public Collection getChatters() { + return participants.values().stream() + .map(ChatterSession::getChatParticipant) + .collect(Collectors.toList()); + } + + public Optional playerLeft(final WebSocketSession session) { + return Optional.ofNullable(participants.remove(session.getId())) + .map(ChatterSession::getChatParticipant) + .map(ChatParticipant::getUserName); + } + + public boolean isPlayerConnected(final UserName userName) { + return participants.values().stream() + .map(ChatterSession::getChatParticipant) + .map(ChatParticipant::getUserName) + .anyMatch(userName::equals); + } + + public Collection fetchOpenSessions() { + return participants.values().stream() + .map(ChatterSession::getSession) + .filter(WebSocketSession::isOpen) + .collect(Collectors.toSet()); + } + + /** + * Disconnects all sessions belonging to a given player identified by name. A disconnected session + * is closed, the closure will trigger a notification on the client of the disconnected player. + * + * @param userName The name of the player whose sessions will be disconnected. + * @param disconnectMessage Message that will be displayed to the disconnected player. + * @return True if any sessions were disconnected, false if none (indicating player was no longer + * in chat). + */ + public boolean disconnectPlayerByName(final UserName userName, final String disconnectMessage) { + final Set sessions = + participants.values().stream() + .filter( + chatterSession -> + chatterSession.getChatParticipant().getUserName().equals(userName)) + .map(ChatterSession::getSession) + .collect(Collectors.toSet()); + + disconnectSession(sessions, disconnectMessage); + return !sessions.isEmpty(); + } + + private void disconnectSession( + final Collection sessions, final String disconnectMessage) { + // Do session disconnects as its own step to avoid concurrent modification. + sessions.forEach( + session -> + session.close( + new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, disconnectMessage))); + } + + /** + * Disconnects all sessions belonging to a given player identified by IP. A disconnected session + * is closed, the closure will trigger a notification on the client of the disconnected player. + * + * @param ip The IP address of the player whose sessions will be disconnected. + * @param disconnectMessage Message that will be displayed to the disconnected player. + * @return True if any sessions were disconnected, false if none (indicating player was no longer + * in chat). + */ + public boolean disconnectIp(final InetAddress ip, final String disconnectMessage) { + final Set sessions = + participants.values().stream() + .filter(chatterSession -> chatterSession.getIp().equals(ip)) + .map(ChatterSession::getSession) + .collect(Collectors.toSet()); + + disconnectSession(sessions, disconnectMessage); + return !sessions.isEmpty(); + } + + /** + * Checks if a given chatter is currently muted, if so returns the {@code Instant} when the mute + * expires otherwise returns an empty optional + */ + public Optional getPlayerMuteExpiration(final InetAddress inetAddress) { + return getPlayerMuteExpiration(inetAddress, Clock.systemUTC()); + } + + @VisibleForTesting + Optional getPlayerMuteExpiration(final InetAddress inetAddress, final Clock clock) { + // if we have a mute + return Optional.ofNullable( + playerMutes.computeIfPresent( + inetAddress, + (address, existingBan) -> existingBan.isAfter(clock.instant()) ? existingBan : null)); + } + + public void mutePlayer(final PlayerChatId playerChatId, final long muteMinutes) { + mutePlayer(playerChatId, muteMinutes, Clock.systemUTC(), MessageBroadcaster.build()); + } + + @VisibleForTesting + void mutePlayer( + final PlayerChatId playerChatId, + final long muteMinutes, + final Clock clock, + final MessageBroadcaster messageBroadcaster) { + lookupPlayerByChatId(playerChatId) + .ifPresent( + chatterSession -> { + muteIpAddress(chatterSession.getIp(), muteMinutes, clock); + broadCastPlayerMutedMessageToAllPlayer( + chatterSession.getChatParticipant().getUserName(), + muteMinutes, + messageBroadcaster); + }); + } + + private void muteIpAddress(final InetAddress ip, final long muteMinutes, final Clock clock) { + final Instant muteUntil = clock.instant().plus(muteMinutes, ChronoUnit.MINUTES); + playerMutes.put(ip, muteUntil); + } + + private void broadCastPlayerMutedMessageToAllPlayer( + final UserName userName, + final long muteMinutes, + final MessageBroadcaster messageBroadcaster) { + messageBroadcaster.accept( + fetchOpenSessions(), + new ChatEventReceivedMessage( + String.format( + "%s was muted by moderator for %s minutes", userName.getValue(), muteMinutes)) + .toEnvelope()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatMessageListener.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatMessageListener.java new file mode 100644 index 0000000..7694d6b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatMessageListener.java @@ -0,0 +1,74 @@ +package org.triplea.modules.chat.event.processing; + +import java.net.InetAddress; +import java.time.Instant; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.chat.history.LobbyChatHistoryDao; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatSentMessage; +import org.triplea.java.concurrency.AsyncRunner; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; + +@Builder +@Slf4j +public class ChatMessageListener implements Consumer> { + + @Nonnull private final Chatters chatters; + @Nonnull private final LobbyChatHistoryDao lobbyChatHistoryDao; + + public static ChatMessageListener build(final Chatters chatters, final Jdbi jdbi) { + return ChatMessageListener.builder() + .chatters(chatters) + .lobbyChatHistoryDao(jdbi.onDemand(LobbyChatHistoryDao.class)) + .build(); + } + + @Override + public void accept(final WebSocketMessageContext messageContext) { + final InetAddress chatterIp = messageContext.getSenderSession().getRemoteAddress(); + + chatters + .lookupPlayerBySession(messageContext.getSenderSession()) + .ifPresent( + session -> + chatters + .getPlayerMuteExpiration(chatterIp) + .ifPresentOrElse( + expiry -> sendResponseToMutedPlayer(expiry, messageContext), + () -> recordAndBroadcastMessageToAllPlayers(session, messageContext))); + } + + private void sendResponseToMutedPlayer( + final Instant muteExpiry, final WebSocketMessageContext messageContext) { + messageContext.sendResponse( + new ChatEventReceivedMessage(PlayerIsMutedMessage.build(muteExpiry))); + } + + private void recordAndBroadcastMessageToAllPlayers( + final ChatterSession session, final WebSocketMessageContext messageContext) { + final var chatReceivedMessage = + convertMessage(session.getChatParticipant(), messageContext.getMessage().getChatMessage()); + recordInHistory(chatReceivedMessage, session); + messageContext.broadcastMessage(chatReceivedMessage); + } + + private static ChatReceivedMessage convertMessage( + final ChatParticipant sender, final String message) { + return new ChatReceivedMessage(sender.getUserName(), message); + } + + private void recordInHistory( + final ChatReceivedMessage chatReceivedMessage, final ChatterSession session) { + AsyncRunner.runAsync( + () -> lobbyChatHistoryDao.recordMessage(chatReceivedMessage, session.getApiKeyId())) + .exceptionally(e -> log.error("Error recording chat message in database history table", e)); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapter.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapter.java new file mode 100644 index 0000000..942ab23 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapter.java @@ -0,0 +1,33 @@ +package org.triplea.modules.chat.event.processing; + +import java.util.function.BiFunction; +import org.triplea.db.dao.api.key.PlayerApiKeyLookupRecord; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.web.socket.WebSocketSession; + +class ChatParticipantAdapter + implements BiFunction { + + @Override + public ChatterSession apply( + final WebSocketSession session, final PlayerApiKeyLookupRecord apiKeyLookupRecord) { + return ChatterSession.builder() + .apiKeyId(apiKeyLookupRecord.getApiKeyId()) + .chatParticipant(buildChatParticipant(apiKeyLookupRecord)) + .session(session) + .ip(session.getRemoteAddress()) + .build(); + } + + private ChatParticipant buildChatParticipant(final PlayerApiKeyLookupRecord apiKeyLookupRecord) { + return ChatParticipant.builder() + .userName(apiKeyLookupRecord.getUsername()) + .isModerator( + apiKeyLookupRecord.getUserRole().equals(UserRole.ADMIN) + || apiKeyLookupRecord.getUserRole().equals(UserRole.MODERATOR)) + .playerChatId(apiKeyLookupRecord.getPlayerChatId()) + .build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerConnectedListener.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerConnectedListener.java new file mode 100644 index 0000000..de18439 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerConnectedListener.java @@ -0,0 +1,57 @@ +package org.triplea.modules.chat.event.processing; + +import java.util.function.Consumer; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatterListingMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ConnectToChatMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerJoinedMessage; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; + +@Builder +public class PlayerConnectedListener + implements Consumer> { + + private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + private final ChatParticipantAdapter chatParticipantAdapter; + private final Chatters chatters; + + public static PlayerConnectedListener build(final Chatters chatters, final Jdbi jdbi) { + return PlayerConnectedListener.builder() + .apiKeyDaoWrapper(PlayerApiKeyDaoWrapper.build(jdbi)) + .chatParticipantAdapter(new ChatParticipantAdapter()) + .chatters(chatters) + .build(); + } + + @Override + public void accept(final WebSocketMessageContext context) { + // Make sure chatter has logged in (has a valid API key) + // Based on the API key we'll know if the player is a moderator. + apiKeyDaoWrapper + .lookupByApiKey(context.getMessage().getApiKey()) + .ifPresent( + keyLookup -> { + final ChatterSession chatterSession = + chatParticipantAdapter.apply(context.getSenderSession(), keyLookup); + final boolean alreadyConnected = + chatters.isPlayerConnected(chatterSession.getChatParticipant().getUserName()); + + // connect the current chatter session if they are connected or not + chatters.connectPlayer(chatterSession); + + context.sendResponse(new ChatterListingMessage(chatters.getChatters())); + + // if the player was already connected, do not send a seemingly + // duplicate player joined message. Registered users can have multiple + // chat sessions and will appear under a single name. + if (!alreadyConnected) { + context.broadcastMessage( + new PlayerJoinedMessage(chatterSession.getChatParticipant())); + } + }); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessage.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessage.java new file mode 100644 index 0000000..4da033d --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessage.java @@ -0,0 +1,37 @@ +package org.triplea.modules.chat.event.processing; + +import com.google.common.annotations.VisibleForTesting; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.function.Function; +import lombok.Builder; +import lombok.experimental.UtilityClass; + +@UtilityClass +class PlayerIsMutedMessage { + private static final Function muteDurationFormatter = + MuteDurationRemainingCalculator.builder().build(); + + String build(final Instant muteExpiry) { + return "You have been muted, expiring in: " + muteDurationFormatter.apply(muteExpiry); + } + + /** + * Calculates a string of how many minutes are remaining in a mute until expired, or if less than + * a minute then how many seconds are left. + */ + @VisibleForTesting + @Builder + static class MuteDurationRemainingCalculator implements Function { + @Builder.Default private Clock clock = Clock.systemUTC(); + + @Override + public String apply(final Instant muteExpiry) { + final long minutes = Duration.between(clock.instant(), muteExpiry).toMinutes(); + return minutes > 0 + ? minutes + " minutes" + : Duration.between(clock.instant(), muteExpiry).toSeconds() + " seconds"; + } + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerLeftListener.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerLeftListener.java new file mode 100644 index 0000000..f35913c --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/PlayerLeftListener.java @@ -0,0 +1,27 @@ +package org.triplea.modules.chat.event.processing; + +import java.util.function.BiConsumer; +import lombok.AllArgsConstructor; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerLeftMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; +import org.triplea.web.socket.WebSocketSession; + +@AllArgsConstructor +public class PlayerLeftListener implements BiConsumer { + + private final Chatters chatters; + + @Override + public void accept( + final WebSocketMessagingBus webSocketMessagingBus, final WebSocketSession session) { + chatters + .playerLeft(session) + .ifPresent( + playerName -> { + if (!chatters.isPlayerConnected(playerName)) { + webSocketMessagingBus.broadcastMessage(new PlayerLeftMessage(playerName)); + } + }); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/SlapListener.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/SlapListener.java new file mode 100644 index 0000000..71e772a --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/SlapListener.java @@ -0,0 +1,34 @@ +package org.triplea.modules.chat.event.processing; + +import java.util.function.Consumer; +import lombok.AllArgsConstructor; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapSentMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; + +@AllArgsConstructor +public class SlapListener implements Consumer> { + private final Chatters chatters; + + @Override + public void accept(final WebSocketMessageContext messageContext) { + chatters + .lookupPlayerBySession(messageContext.getSenderSession()) + .ifPresent( + chatterSession -> + broadCastSlapMessage(messageContext, chatterSession.getChatParticipant())); + } + + private static void broadCastSlapMessage( + final WebSocketMessageContext messageContext, + final ChatParticipant slapper) { + + messageContext.broadcastMessage( + PlayerSlapReceivedMessage.builder() + .slappingPlayer(slapper.getUserName().getValue()) + .slappedPlayer(messageContext.getMessage().getSlappedPlayer()) + .build()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/StatusUpdateListener.java b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/StatusUpdateListener.java new file mode 100644 index 0000000..87caa81 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/chat/event/processing/StatusUpdateListener.java @@ -0,0 +1,34 @@ +package org.triplea.modules.chat.event.processing; + +import java.util.function.Consumer; +import lombok.AllArgsConstructor; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateSentMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; + +@AllArgsConstructor +public class StatusUpdateListener + implements Consumer> { + + private final Chatters chatters; + + @Override + public void accept(final WebSocketMessageContext messageContext) { + chatters + .lookupPlayerBySession(messageContext.getSenderSession()) + .ifPresent( + chatterSession -> + broadcastStatusUpdateMessage(messageContext, chatterSession.getChatParticipant())); + } + + private static void broadcastStatusUpdateMessage( + final WebSocketMessageContext messageContext, + final ChatParticipant chatParticipant) { + + messageContext.broadcastMessage( + new PlayerStatusUpdateReceivedMessage( + chatParticipant.getUserName(), messageContext.getMessage().getStatus())); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/ForgotPasswordModule.java b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/ForgotPasswordModule.java new file mode 100644 index 0000000..52197e6 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/ForgotPasswordModule.java @@ -0,0 +1,79 @@ +package org.triplea.modules.forgot.password; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.temp.password.TempPasswordHistoryDao; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; +import org.triplea.java.StringUtils; + +/** + * Module to orchestrate generating a temporary password for a user, storing that password in the + * temp password table and sending that password in an email to the user. + */ +@Builder +public class ForgotPasswordModule implements BiFunction { + + @VisibleForTesting + static final String ERROR_TOO_MANY_REQUESTS = "Error, too many password reset attempts"; + + @VisibleForTesting + static final String ERROR_BAD_USER_OR_EMAIL = + "Error, temp password not generated - check username and email"; + + @VisibleForTesting + static final String SUCCESS_MESSAGE = "A temporary password has been sent to your email."; + + @Nonnull private final BiConsumer passwordEmailSender; + @Nonnull private final PasswordGenerator passwordGenerator; + @Nonnull private final TempPasswordPersistence tempPasswordPersistence; + @Nonnull private final TempPasswordHistory tempPasswordHistory; + + public static BiFunction build( + final boolean isProd, final Jdbi jdbi) { + return ForgotPasswordModule.builder() + .passwordEmailSender(PasswordEmailSender.builder().isProd(isProd).build()) + .passwordGenerator(new PasswordGenerator()) + .tempPasswordPersistence(TempPasswordPersistence.newInstance(jdbi)) + .tempPasswordHistory(new TempPasswordHistory(jdbi.onDemand(TempPasswordHistoryDao.class))) + .build(); + } + + @Override + public String apply(final String inetAddress, final ForgotPasswordRequest forgotPasswordRequest) { + checkArgs(inetAddress, forgotPasswordRequest); + + if (!tempPasswordHistory.allowRequestFromAddress(inetAddress)) { + return ERROR_TOO_MANY_REQUESTS; + } + tempPasswordHistory.recordTempPasswordRequest(inetAddress, forgotPasswordRequest.getUsername()); + + final String generatedPassword = passwordGenerator.generatePassword(); + if (!tempPasswordPersistence.storeTempPassword(forgotPasswordRequest, generatedPassword)) { + return ERROR_BAD_USER_OR_EMAIL; + } + passwordEmailSender.accept(forgotPasswordRequest.getEmail(), generatedPassword); + + return SUCCESS_MESSAGE; + } + + private static void checkArgs( + final String inetAddress, final ForgotPasswordRequest forgotPasswordRequest) { + checkArgument(!StringUtils.isNullOrBlank(inetAddress)); + try { + //noinspection ResultOfMethodCallIgnored + InetAddress.getAllByName(inetAddress); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException("Invalid IP address: " + inetAddress); + } + checkArgument(!StringUtils.isNullOrBlank(forgotPasswordRequest.getEmail())); + checkArgument(!StringUtils.isNullOrBlank(forgotPasswordRequest.getUsername())); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordEmailSender.java b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordEmailSender.java new file mode 100644 index 0000000..02a678a --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordEmailSender.java @@ -0,0 +1,70 @@ +package org.triplea.modules.forgot.password; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import java.io.UnsupportedEncodingException; +import java.util.Properties; +import java.util.function.BiConsumer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.triplea.db.dao.temp.password.TempPasswordDao; + +/** Sends a temporary password to a target user. */ +@AllArgsConstructor +@Builder +@Slf4j +class PasswordEmailSender implements BiConsumer { + private static final String FROM = "no-reply@triplea-game.org"; + + private final boolean isProd; + + @Override + public void accept(final String email, final String generatedPassword) { + if (!isProd) { + // do not send emails if not on prod + log.info( + String.format( + "Non-prod forgot password, email: %s, generated temp passsword: %s", + email, generatedPassword)); + return; + } + + final Properties props = new Properties(); + final Session session = Session.getDefaultInstance(props, null); + + try { + final Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(FROM, "no-reply")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress(email, email)); + message.setSubject("Temporary Password"); + message.setText(createMailBody(generatedPassword)); + Transport.send(message); + } catch (final MessagingException e) { + throw new EmailSendFailure(e); + } catch (final UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + private static String createMailBody(final String generatedPassword) { + return "Your TripleA temporary password is: " + + generatedPassword + + "\nUse this password to login to the TripleA lobby." + + "\nThis password may only be used once and will expire in " + + TempPasswordDao.TEMP_PASSWORD_EXPIRATION + + "\nAfter login you will be prompted to create a new password"; + } + + private static class EmailSendFailure extends RuntimeException { + private static final long serialVersionUID = -7190784731567635418L; + + EmailSendFailure(final Exception e) { + super("Failed to send email", e); + } + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordGenerator.java b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordGenerator.java new file mode 100644 index 0000000..d7c3ee1 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/PasswordGenerator.java @@ -0,0 +1,13 @@ +package org.triplea.modules.forgot.password; + +import com.google.common.annotations.VisibleForTesting; +import java.util.UUID; + +/** Generates a plaintext password to be stored as a temporary password. */ +class PasswordGenerator { + @VisibleForTesting static final int PASSWORD_LENGTH = 18; + + String generatePassword() { + return UUID.randomUUID().toString().substring(0, PASSWORD_LENGTH); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordHistory.java b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordHistory.java new file mode 100644 index 0000000..c0b0899 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordHistory.java @@ -0,0 +1,25 @@ +package org.triplea.modules.forgot.password; + +import com.google.common.annotations.VisibleForTesting; +import lombok.AllArgsConstructor; +import org.triplea.db.dao.temp.password.TempPasswordHistoryDao; + +/** + * Class for accessing temp password history. The history is used for audit and rate limiting so a + * user cannot create unlimited temp passwords requests. + */ +@AllArgsConstructor +public class TempPasswordHistory { + @VisibleForTesting static final int MAX_TEMP_PASSWORD_REQUESTS_PER_DAY = 3; + + private final TempPasswordHistoryDao tempPasswordHistoryDao; + + boolean allowRequestFromAddress(final String address) { + return tempPasswordHistoryDao.countRequestsFromAddress(address) + < MAX_TEMP_PASSWORD_REQUESTS_PER_DAY; + } + + public void recordTempPasswordRequest(final String address, final String username) { + tempPasswordHistoryDao.recordTempPasswordRequest(address, username); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordPersistence.java b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordPersistence.java new file mode 100644 index 0000000..89ef943 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/forgot/password/TempPasswordPersistence.java @@ -0,0 +1,40 @@ +package org.triplea.modules.forgot.password; + +import com.google.common.annotations.VisibleForTesting; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.temp.password.TempPasswordDao; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; +import org.triplea.java.Sha512Hasher; +import org.triplea.modules.user.account.PasswordBCrypter; + +/** + * Stores a user temporary password in database. When we generate a new temporary password, all + * existing temporary passwords are invalidated so that a user only has one temp password at a time. + */ +@AllArgsConstructor( + access = AccessLevel.PACKAGE, + onConstructor_ = {@VisibleForTesting}) +class TempPasswordPersistence { + @Nonnull private final TempPasswordDao tempPasswordDao; + @Nonnull private final Function passwordHasher; + @Nonnull private final Function hashedPasswordBcrypter; + + static TempPasswordPersistence newInstance(final Jdbi jdbi) { + return new TempPasswordPersistence( + jdbi.onDemand(TempPasswordDao.class), + Sha512Hasher::hashPasswordWithSalt, + PasswordBCrypter::hashPassword); + } + + boolean storeTempPassword( + final ForgotPasswordRequest forgotPasswordRequest, final String generatedPassword) { + final String hashedPass = passwordHasher.apply(generatedPassword); + final String tempPass = hashedPasswordBcrypter.apply(hashedPass); + return tempPasswordDao.insertTempPassword( + forgotPasswordRequest.getUsername(), forgotPasswordRequest.getEmail(), tempPass); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameListing.java b/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameListing.java new file mode 100644 index 0000000..de1c0e2 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameListing.java @@ -0,0 +1,234 @@ +package org.triplea.modules.game.listing; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.lobby.games.LobbyGameDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.LobbyGame; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.game.lobby.watcher.GameListingClient; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameUpdatedMessage; +import org.triplea.java.cache.ttl.ExpiringAfterWriteTtlCache; +import org.triplea.java.cache.ttl.TtlCache; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** + * Class that stores the set of games in the lobby. Games are identified by a combination of two + * values, the api-key of the user that created the game and a UUID assigned when the game is + * posted. Keep in mind that the 'gameId' is a publicly known value. Therefore we use API key, for + * example, to ensure that other users can't invoke endpoints directly to remove the games of other + * players.
+ * + *

Keep-Alive

+ * + * The game listing has a concept of 'keep-alive' where games, after posting, need to send + * 'keep-alive' messages periodically or they will be de-listed. If a game has been de-listed, and + * we get a 'keep-alive' message, the client will get a 'false' value back indicating they would + * need to (automatically) re-post their game. This way for example if the lobby were to be + * restarted, clients sending keep-alives would notice that their game is no longer listed and would + * automatically send a game post message to re-post their game. + * + *

Moderator Boot Game

+ * + * The moderator boot is similar to remove game but there is no check for an API key, any moderator + * can boot any game. + */ +@Builder +@Slf4j +public class GameListing { + @Nonnull private final ModeratorAuditHistoryDao auditHistoryDao; + @Nonnull private final LobbyGameDao lobbyGameDao; + @Nonnull private final TtlCache games; + @Nonnull private final WebSocketMessagingBus playerMessagingBus; + + /** Map of player names to the games they are in, both observing and playing. */ + @Nonnull private final Multimap playerIsInGames = HashMultimap.create(); + + @AllArgsConstructor + @EqualsAndHashCode + @Getter + @VisibleForTesting + @ToString + static class GameId { + @Nonnull private final ApiKey apiKey; + @Nonnull private final String id; + } + + public static GameListing build(final Jdbi jdbi, final WebSocketMessagingBus playerMessagingBus) { + return GameListing.builder() + .lobbyGameDao(jdbi.onDemand(LobbyGameDao.class)) + .auditHistoryDao(jdbi.onDemand(ModeratorAuditHistoryDao.class)) + .playerMessagingBus(playerMessagingBus) + .games( + new ExpiringAfterWriteTtlCache<>( + GameListingClient.KEEP_ALIVE_SECONDS, + TimeUnit.SECONDS, + new GameTtlExpiredListener(playerMessagingBus))) + .build(); + } + + /** Adds a game. */ + public String postGame(final ApiKey apiKey, final GamePostingRequest gamePostingRequest) { + final String id = UUID.randomUUID().toString(); + final GameId gameId = new GameId(apiKey, id); + games.put(gameId, gamePostingRequest.getLobbyGame()); + + Optional.ofNullable(gamePostingRequest.getPlayerNames()) + .ifPresent( + names -> + names.forEach(playerName -> playerIsInGames.put(UserName.of(playerName), gameId))); + final var lobbyGameListing = + LobbyGameListing.builder().gameId(id).lobbyGame(gamePostingRequest.getLobbyGame()).build(); + lobbyGameDao.insertLobbyGame(apiKey, lobbyGameListing); + playerMessagingBus.broadcastMessage(new LobbyGameUpdatedMessage(lobbyGameListing)); + log.info("Posted game: {}", id); + return id; + } + + /** Adds or updates a game. Returns true if game is updated, false if game was not found. */ + public boolean updateGame(final ApiKey apiKey, final String id, final LobbyGame lobbyGame) { + final var listedGameId = new GameId(apiKey, id); + final LobbyGame existingValue = games.replace(listedGameId, lobbyGame).orElse(null); + + if (existingValue != null) { + playerMessagingBus.broadcastMessage( + new LobbyGameUpdatedMessage( + LobbyGameListing.builder().gameId(id).lobbyGame(lobbyGame).build())); + return true; + } else { + return false; + } + } + + /** + * Removes a game from the active listing, any players marked as in the game are updated to no + * longer be listed as participating in that game. + */ + public void removeGame(final ApiKey apiKey, final String id) { + log.info("Removing game: {}", id); + final GameId key = new GameId(apiKey, id); + + final var gameEntries = + playerIsInGames.entries().stream() + .filter(entry -> entry.getValue().equals(key)) + .collect(Collectors.toList()); + gameEntries.forEach(entry -> playerIsInGames.remove(entry.getKey(), entry.getValue())); + + games + .invalidate(key) + .ifPresent(value -> playerMessagingBus.broadcastMessage(new LobbyGameRemovedMessage(id))); + } + + public List getGames() { + return games.asMap().entrySet().stream() + .map( + entry -> + LobbyGameListing.builder() + .gameId(entry.getKey().id) + .lobbyGame(entry.getValue()) + .build()) + .collect(Collectors.toList()); + } + + /** Checks if a given api-key and game-id pair are valid and match an active game. */ + public boolean isValidApiKeyAndGameId(final ApiKey apiKey, final String gameId) { + return games.get(new GameId(apiKey, gameId)).isPresent(); + } + + /** + * If a game does not receive a 'keepAlive' in a timely manner, it is removed from the list. + * + * @return Return false to inform client game is not present. Client can respond by re-posting + * their game. Otherwise true indicates the game is present and the keep-alive period has been + * extended. + */ + public boolean keepAlive(final ApiKey apiKey, final String id) { + return games.refresh(new GameId(apiKey, id)); + } + + /** Moderator action to remove a game. */ + public void bootGame(final int moderatorId, final String id) { + games + .findEntryByKey(gameId -> gameId.id.equals(id)) + .ifPresent( + gameToRemove -> { + final String hostName = gameToRemove.getValue().getHostName(); + removeGame(gameToRemove.getKey().apiKey, gameToRemove.getKey().id); + + log.info("Moderator {} booted game: {}, hosted by: {}", moderatorId, id, hostName); + auditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorId) + .actionName(ModeratorAuditHistoryDao.AuditAction.BOOT_GAME) + .actionTarget(hostName) + .build()); + }); + } + + public Optional getHostForGame(final ApiKey apiKey, final String id) { + return games + .get(new GameId(apiKey, id)) + .map( + lobbyGame -> + new InetSocketAddress(lobbyGame.getHostAddress(), lobbyGame.getHostPort())); + } + + public void addPlayerToGame(final UserName userName, final ApiKey apiKey, final String gameId) { + playerIsInGames.put(userName, new GameId(apiKey, gameId)); + } + + public void removePlayerFromGame( + final UserName userName, final ApiKey apiKey, final String gameId) { + playerIsInGames.remove(userName, new GameId(apiKey, gameId)); + } + + /** + * Gets the collection of active games (identified by hostname) that a player is playing in or has + * joined as an observer. + */ + public Collection getGameNamesPlayerHasJoined(final UserName userName) { + final Collection expiredGames = + playerIsInGames.get(userName).stream() + .filter(gameId -> games.get(gameId).isEmpty()) + .collect(Collectors.toList()); + expiredGames.forEach(gameId -> playerIsInGames.remove(userName, gameId)); + + return playerIsInGames.get(userName).stream() + .map(gameId -> games.get(gameId).map(LobbyGame::getHostName).orElse(null)) + .collect(Collectors.toList()); + } + + public Collection getPlayersInGame(final String gameId) { + return playerIsInGames.asMap().entrySet().stream() + .filter(playerIsInGame(gameId)) + .map(Map.Entry::getKey) + .map(UserName::getValue) + .collect(Collectors.toList()); + } + + private Predicate>> playerIsInGame(final String gameId) { + return entry -> entry.getValue().stream().map(GameId::getId).anyMatch(gameId::equals); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameTtlExpiredListener.java b/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameTtlExpiredListener.java new file mode 100644 index 0000000..744cbd6 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/game/listing/GameTtlExpiredListener.java @@ -0,0 +1,18 @@ +package org.triplea.modules.game.listing; + +import java.util.function.BiConsumer; +import lombok.AllArgsConstructor; +import org.triplea.domain.data.LobbyGame; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage; +import org.triplea.web.socket.WebSocketMessagingBus; + +@AllArgsConstructor +class GameTtlExpiredListener implements BiConsumer { + + private final WebSocketMessagingBus playerMessagingBus; + + @Override + public void accept(final GameListing.GameId gameId, final LobbyGame removedEntry) { + playerMessagingBus.broadcastMessage(new LobbyGameRemovedMessage(gameId.getId())); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ChatUploadModule.java b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ChatUploadModule.java new file mode 100644 index 0000000..7e96a95 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ChatUploadModule.java @@ -0,0 +1,42 @@ +package org.triplea.modules.game.lobby.watcher; + +import java.util.function.BiPredicate; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.lobby.games.LobbyGameDao; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatMessageUpload; +import org.triplea.modules.game.listing.GameListing; + +/** + * Responsible for inserting chat messages into database. Validates that a given request has a + * matching game-id and api-key pair, otherwise does a no-op. We need to be sure to validate an + * API-key and game-id match otherwise an attacker with an http client could try to insert incorrect + * data, game-id's are publicly known, but API-keys are not. Only the actual host should know the + * api-key. + * + *

If the ID and API-Key pair are not valid, we will just drop the request and return a 200 If we + * return a 400 or some other error, we'll give a potential attacker a way to try and guess + * API-Keys. + */ +@AllArgsConstructor +public class ChatUploadModule { + private final LobbyGameDao lobbyGameDao; + private final BiPredicate gameIdValidator; + + public static ChatUploadModule build(final Jdbi jdbi, final GameListing gameListing) { + return new ChatUploadModule( + jdbi.onDemand(LobbyGameDao.class), gameListing::isValidApiKeyAndGameId); + } + + public boolean upload(final String apiKey, final ChatMessageUpload chatMessageUpload) { + if (gameIdValidator.test( + // truncate 'Bearer ' from the apiKey if + ApiKey.of(apiKey.startsWith("Bearer ") ? apiKey.substring("Bearer ".length()) : apiKey), + chatMessageUpload.getGameId())) { + lobbyGameDao.recordChat(chatMessageUpload); + return true; + } + return false; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheck.java b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheck.java new file mode 100644 index 0000000..6ff2ce3 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheck.java @@ -0,0 +1,35 @@ +package org.triplea.modules.game.lobby.watcher; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; + +/** + * Performs a 'reverse' connection back to a game host to ensure that they can be connected to from + * the public internet (required if others are to join their game). + */ +@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor_ = @VisibleForTesting) +class ConnectivityCheck { + + private final Supplier socketSupplier; + + ConnectivityCheck() { + this(Socket::new); + } + + /** Checks if the lobby can create a connection to a given game. */ + boolean canDoReverseConnect(final String gameHostAddress, final int port) { + final InetSocketAddress address = new InetSocketAddress(gameHostAddress, port); + try (Socket s = socketSupplier.get()) { + s.connect(address, (int) TimeUnit.SECONDS.toMillis(10)); + return s.isConnected(); + } catch (final IOException e) { + return false; + } + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/GamePostingModule.java b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/GamePostingModule.java new file mode 100644 index 0000000..d086bfd --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/game/lobby/watcher/GamePostingModule.java @@ -0,0 +1,42 @@ +package org.triplea.modules.game.lobby.watcher; + +import javax.annotation.Nonnull; +import lombok.Builder; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingResponse; +import org.triplea.modules.game.listing.GameListing; + +/** + * Verifies that we can do a 'reverse' connection back to the requesting game host (this verifies + * that their network is well configured to accept connections), if so then we will add their game + * posting request to the list of available games. + */ +@Builder +public class GamePostingModule { + @Nonnull private final GameListing gameListing; + @Nonnull private final ConnectivityCheck connectivityCheck; + + public static GamePostingModule build(final GameListing gameListing) { + + return GamePostingModule.builder() + .gameListing(gameListing) + .connectivityCheck(new ConnectivityCheck()) + .build(); + } + + public GamePostingResponse postGame( + final ApiKey apiKey, final GamePostingRequest gamePostingRequest) { + final boolean canReverseConnect = + connectivityCheck.canDoReverseConnect( + gamePostingRequest.getLobbyGame().getHostAddress(), + gamePostingRequest.getLobbyGame().getHostPort()); + + return canReverseConnect + ? GamePostingResponse.builder() + .connectivityCheckSucceeded(true) + .gameId(gameListing.postGame(apiKey, gamePostingRequest)) + .build() + : GamePostingResponse.builder().connectivityCheckSucceeded(false).build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/access/log/AccessLogService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/access/log/AccessLogService.java new file mode 100644 index 0000000..5d77fd7 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/access/log/AccessLogService.java @@ -0,0 +1,42 @@ +package org.triplea.modules.moderation.access.log; + +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.access.log.AccessLogDao; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogData; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogRequest; + +@Builder +public class AccessLogService { + @Nonnull private final AccessLogDao accessLogDao; + + public static AccessLogService build(final Jdbi jdbi) { + return AccessLogService.builder() // + .accessLogDao(jdbi.onDemand(AccessLogDao.class)) + .build(); + } + + public List fetchAccessLog(final AccessLogRequest accessLogRequest) { + return accessLogDao + .fetchAccessLogRows( + accessLogRequest.getPagingParams().getRowNumber(), + accessLogRequest.getPagingParams().getPageSize(), + accessLogRequest.getAccessLogSearchRequest().getUsername(), + accessLogRequest.getAccessLogSearchRequest().getIp(), + accessLogRequest.getAccessLogSearchRequest().getSystemId()) + .stream() + .map( + daoData -> + AccessLogData.builder() + .accessDate(daoData.getAccessTime().toEpochMilli()) + .username(daoData.getUsername()) + .ip(daoData.getIp()) + .systemId(daoData.getSystemId()) + .registered(daoData.isRegistered()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryService.java new file mode 100644 index 0000000..37e56d7 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryService.java @@ -0,0 +1,30 @@ +package org.triplea.modules.moderation.audit.history; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.http.client.lobby.moderator.toolbox.log.ModeratorEvent; + +@AllArgsConstructor +public class ModeratorAuditHistoryService { + private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + public static ModeratorAuditHistoryService build(final Jdbi jdbi) { + return new ModeratorAuditHistoryService(jdbi.onDemand(ModeratorAuditHistoryDao.class)); + } + + public List lookupHistory(final int rowNumber, final int rowCount) { + return moderatorAuditHistoryDao.lookupHistoryItems(rowNumber, rowCount).stream() + .map( + item -> + ModeratorEvent.builder() + .date(item.getDateCreated().toEpochMilli()) + .moderatorName(item.getUsername()) + .moderatorAction(item.getActionName()) + .actionTarget(item.getActionTarget()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/bad/words/BadWordsService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/bad/words/BadWordsService.java new file mode 100644 index 0000000..5018aee --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/bad/words/BadWordsService.java @@ -0,0 +1,59 @@ +package org.triplea.modules.moderation.bad.words; + +import java.util.List; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.BadWordsDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; + +@AllArgsConstructor +public class BadWordsService { + private final BadWordsDao badWordsDao; + private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + public static BadWordsService build(final Jdbi jdbi) { + return new BadWordsService( + jdbi.onDemand(BadWordsDao.class), jdbi.onDemand(ModeratorAuditHistoryDao.class)); + } + + /** + * Removes a bad word value from the bad-word table in database. + * + * @param moderatorUserId Database ID of the moderator requesting the action. + * @param badWord The value to be removed. + */ + public void removeBadWord(final int moderatorUserId, final String badWord) { + badWordsDao.removeBadWord(badWord); + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorUserId) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_BAD_WORD) + .actionTarget(badWord) + .build()); + } + + /** + * Adds a bad word value to the bad-word table. + * + * @param moderatorUserId Database ID of the moderator requesting the action. + * @param badWord The value to add. + * @return True if the value is added, false otherwise (eg: value might already exist in DB). + */ + public boolean addBadWord(final int moderatorUserId, final String badWord) { + final boolean success = badWordsDao.addBadWord(badWord) == 1; + if (success) { + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorUserId) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_BAD_WORD) + .actionTarget(badWord) + .build()); + } + return success; + } + + /** Returns the list of bad words present in the bad-word table. */ + public List getBadWords() { + return badWordsDao.getBadWords(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/name/UsernameBanService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/name/UsernameBanService.java new file mode 100644 index 0000000..a96cbe4 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/name/UsernameBanService.java @@ -0,0 +1,62 @@ +package org.triplea.modules.moderation.ban.name; + +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.username.ban.UsernameBanDao; +import org.triplea.http.client.lobby.moderator.toolbox.banned.name.UsernameBanData; + +@AllArgsConstructor +@Builder +public class UsernameBanService { + @Nonnull private final UsernameBanDao bannedUserNamesDao; + @Nonnull private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + public static UsernameBanService build(final Jdbi jdbi) { + return UsernameBanService.builder() + .bannedUserNamesDao(jdbi.onDemand(UsernameBanDao.class)) + .moderatorAuditHistoryDao(jdbi.onDemand(ModeratorAuditHistoryDao.class)) + .build(); + } + + public boolean removeUsernameBan(final int moderatorUserId, final String nameToUnBan) { + if (bannedUserNamesDao.removeBannedUserName(nameToUnBan) > 0) { + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorUserId) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_USERNAME_BAN) + .actionTarget(nameToUnBan) + .build()); + return true; + } + return false; + } + + public boolean addBannedUserName(final int moderatorUserId, final String nameToBan) { + if (bannedUserNamesDao.addBannedUserName(nameToBan) == 1) { + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorUserId) + .actionName(ModeratorAuditHistoryDao.AuditAction.BAN_USERNAME) + .actionTarget(nameToBan) + .build()); + return true; + } + return false; + } + + public List getBannedUserNames() { + return bannedUserNamesDao.getBannedUserNames().stream() + .map( + data -> + UsernameBanData.builder() + .bannedName(data.getUsername()) + .banDate(data.getDateCreated().toEpochMilli()) + .build()) + .collect(Collectors.toList()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandler.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandler.java new file mode 100644 index 0000000..5c4cb6b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandler.java @@ -0,0 +1,23 @@ +package org.triplea.modules.moderation.ban.user; + +import java.net.InetAddress; +import java.util.Collection; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.triplea.web.socket.SessionSet; + +/** + * Provides a callback mechanism to close all sessions of a banned player (banned by IP). Each + * websocket will be provided a {@code SessionSet} where any new sessions will be registered, each + * such {@code SessionSet} is also registered here and then receives a callback to close any + * sessions for a banned IP. + */ +@Builder +public class BannedPlayerEventHandler { + + @Nonnull private final Collection sessionSets; + + public void fireBannedEvent(final InetAddress bannedIp) { + sessionSets.forEach(sessionSet -> sessionSet.closeSessionsByIp(bannedIp)); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/UserBanService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/UserBanService.java new file mode 100644 index 0000000..b4c4d9f --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/ban/user/UserBanService.java @@ -0,0 +1,164 @@ +package org.triplea.modules.moderation.ban.user; + +import com.google.common.annotations.VisibleForTesting; +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.BanDurationFormatter; +import org.triplea.http.client.lobby.moderator.BanPlayerRequest; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanData; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanParams; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.remote.actions.PlayerBannedMessage; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** + * Service layer for managing user bans, get bans, add and remove. User bans are done by MAC and IP + * address, they are removed by the 'public ban id' that is assigned when a ban is issued. + */ +@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor_ = @VisibleForTesting) +public class UserBanService { + + private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + private final UserBanDao userBanDao; + private final Supplier publicIdSupplier; + private final Chatters chatters; + private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + private final WebSocketMessagingBus chatMessagingBus; + private final WebSocketMessagingBus gameMessagingBus; + + @Builder + public UserBanService( + final Jdbi jdbi, + final Chatters chatters, + final WebSocketMessagingBus chatMessagingBus, + final WebSocketMessagingBus gameMessagingBus) { + moderatorAuditHistoryDao = jdbi.onDemand(ModeratorAuditHistoryDao.class); + userBanDao = jdbi.onDemand(UserBanDao.class); + publicIdSupplier = () -> UUID.randomUUID().toString(); + this.chatters = chatters; + this.apiKeyDaoWrapper = PlayerApiKeyDaoWrapper.build(jdbi); + this.chatMessagingBus = chatMessagingBus; + this.gameMessagingBus = gameMessagingBus; + } + + public List getBannedUsers() { + return userBanDao.lookupBans().stream() + .map( + daoData -> + UserBanData.builder() + .banId(daoData.getPublicBanId()) + .username(daoData.getUsername()) + .hashedMac(daoData.getSystemId()) + .ip(daoData.getIp()) + .banDate(daoData.getDateCreated().toEpochMilli()) + .banExpiry(daoData.getBanExpiry().toEpochMilli()) + .build()) + .collect(Collectors.toList()); + } + + public boolean removeUserBan(final int moderatorId, final String banId) { + final String unbanName = userBanDao.lookupUsernameByBanId(banId).orElse(null); + if (userBanDao.removeBan(banId) != 1) { + return false; + } + if (unbanName == null) { + throw new IllegalStateException( + "Consistency error, unbanned " + + banId + + ", but " + + "there was no matching name for that ban."); + } + + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_USER_BAN) + .actionTarget(unbanName) + .moderatorUserId(moderatorId) + .build()); + return true; + } + + public void banUser(final int moderatorId, final BanPlayerRequest banPlayerRequest) { + apiKeyDaoWrapper + .lookupPlayerByChatId(PlayerChatId.of(banPlayerRequest.getPlayerChatId())) + .map( + gamePlayerLookup -> toUserBanParams(gamePlayerLookup, banPlayerRequest.getBanMinutes())) + .ifPresent(banUserParams -> banUser(moderatorId, banUserParams)); + } + + public void banUser(final int moderatorId, final UserBanParams userBanParams) { + persistUserBanToDatabase(userBanParams); + + if (removePlayerFromChat(userBanParams)) { + broadcastToChattersPlayerBannedMessage(userBanParams); + } + + // notify game hosts of the ban + gameMessagingBus.broadcastMessage(new PlayerBannedMessage(userBanParams.getIp())); + + recordBanInModeratorAuditLog(moderatorId, userBanParams); + } + + private static UserBanParams toUserBanParams( + final PlayerIdentifiersByApiKeyLookup gamePlayerLookup, final long banMinutes) { + return UserBanParams.builder() + .systemId(gamePlayerLookup.getSystemId().getValue()) + .username(gamePlayerLookup.getUserName().getValue()) + .ip(gamePlayerLookup.getIp()) + .minutesToBan(banMinutes) + .build(); + } + + private void persistUserBanToDatabase(final UserBanParams userBanParams) { + if (userBanDao.addBan( + publicIdSupplier.get(), + userBanParams.getUsername(), + userBanParams.getSystemId(), + userBanParams.getIp(), + userBanParams.getMinutesToBan()) + != 1) { + throw new IllegalStateException("Failed to insert ban record:" + userBanParams); + } + } + + private boolean removePlayerFromChat(final UserBanParams userBanParams) { + return chatters.disconnectIp( + IpAddressParser.fromString(userBanParams.getIp()), + String.format( + "You have been banned for %s for violating lobby rules", + BanDurationFormatter.formatBanMinutes(userBanParams.getMinutesToBan()))); + } + + private void broadcastToChattersPlayerBannedMessage(final UserBanParams banUserParams) { + chatMessagingBus.broadcastMessage( + new ChatEventReceivedMessage( + String.format( + "%s violated lobby rules and was banned for %s", + banUserParams.getUsername(), + BanDurationFormatter.formatBanMinutes(banUserParams.getMinutesToBan())))); + } + + private void recordBanInModeratorAuditLog( + final int moderatorId, final UserBanParams userBanParams) { + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorId) + .actionName(ModeratorAuditHistoryDao.AuditAction.BAN_USER) + .actionTarget( + userBanParams.getUsername() + " " + userBanParams.getMinutesToBan() + " minutes") + .build()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModule.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModule.java new file mode 100644 index 0000000..1b297d7 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModule.java @@ -0,0 +1,26 @@ +package org.triplea.modules.moderation.chat.history; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.chat.history.ChatHistoryRecord; +import org.triplea.db.dao.moderator.chat.history.GameChatHistoryDao; +import org.triplea.http.client.lobby.moderator.ChatHistoryMessage; + +@AllArgsConstructor +public class FetchGameChatHistoryModule implements Function> { + private final GameChatHistoryDao gameChatHistoryDao; + + public static FetchGameChatHistoryModule build(final Jdbi jdbi) { + return new FetchGameChatHistoryModule(jdbi.onDemand(GameChatHistoryDao.class)); + } + + @Override + public List apply(final String gameId) { + return gameChatHistoryDao.getChatHistory(gameId).stream() + .map(ChatHistoryRecord::toChatHistoryMessage) + .collect(Collectors.toList()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserAction.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserAction.java new file mode 100644 index 0000000..e1740db --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserAction.java @@ -0,0 +1,58 @@ +package org.triplea.modules.moderation.disconnect.user; + +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; + +@Builder +public class DisconnectUserAction { + + @Nonnull private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + @Nonnull private final Chatters chatters; + @Nonnull private final WebSocketMessagingBus playerConnections; + @Nonnull private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + public static DisconnectUserAction build( + final Jdbi jdbi, final Chatters chatters, final WebSocketMessagingBus playerConnections) { + return DisconnectUserAction.builder() + .apiKeyDaoWrapper(PlayerApiKeyDaoWrapper.build(jdbi)) + .chatters(chatters) + .playerConnections(playerConnections) + .moderatorAuditHistoryDao(jdbi.onDemand(ModeratorAuditHistoryDao.class)) + .build(); + } + + /** + * Does a simple disconnect of a given player from chat, records an audit log entry, and notifies + * chatters of the disconnect. + */ + public boolean disconnectPlayer(final int moderatorId, final PlayerChatId playerChatId) { + final PlayerIdentifiersByApiKeyLookup gamePlayerLookup = + apiKeyDaoWrapper.lookupPlayerByChatId(playerChatId).orElse(null); + if (gamePlayerLookup == null || !chatters.isPlayerConnected(gamePlayerLookup.getUserName())) { + return false; + } + + if (chatters.disconnectPlayerByName( + gamePlayerLookup.getUserName(), "Disconnected by moderator")) { + playerConnections.broadcastMessage( + new ChatEventReceivedMessage( + gamePlayerLookup.getUserName() + " was disconnected by moderator")); + } + + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorId) + .actionName(ModeratorAuditHistoryDao.AuditAction.DISCONNECT_USER) + .actionTarget(gamePlayerLookup.getUserName().toString()) + .build()); + return true; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/moderators/ModeratorsService.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/moderators/ModeratorsService.java new file mode 100644 index 0000000..e6fee3b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/moderators/ModeratorsService.java @@ -0,0 +1,113 @@ +package org.triplea.modules.moderation.moderators; + +import com.google.common.base.Preconditions; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.moderator.ModeratorsDao; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.http.client.lobby.moderator.toolbox.management.ModeratorInfo; + +@Builder +@Slf4j +public class ModeratorsService { + @Nonnull private final ModeratorsDao moderatorsDao; + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + public static ModeratorsService build(final Jdbi jdbi) { + return ModeratorsService.builder() + .moderatorsDao(jdbi.onDemand(ModeratorsDao.class)) + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .moderatorAuditHistoryDao(jdbi.onDemand(ModeratorAuditHistoryDao.class)) + .build(); + } + + /** Returns a list of all users that are moderators. */ + public List fetchModerators() { + return moderatorsDao.getModerators().stream() + .map( + userInfo -> + ModeratorInfo.builder() + .name(userInfo.getUsername()) + .lastLoginEpochMillis( + Optional.ofNullable(userInfo.getLastLogin()) + .map(Instant::toEpochMilli) + .orElse(null)) + .build()) + .collect(Collectors.toList()); + } + + /** Promotes a user to moderator. Can only be done by super-moderators. */ + public void addModerator(final int moderatorIdRequesting, final String username) { + final int userId = + userJdbiDao + .lookupUserIdByName(username) + .orElseThrow( + () -> new IllegalArgumentException("Unable to find username: " + username)); + + Preconditions.checkState(moderatorsDao.setRole(userId, UserRole.MODERATOR) == 1); + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorIdRequesting) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_MODERATOR) + .actionTarget(username) + .build()); + log.info(username + " was promoted to moderator"); + } + + /** Removes moderator status from a user. Can only be done by super moderators. */ + public void removeMod(final int moderatorIdRequesting, final String moderatorNameToRemove) { + final int userId = + userJdbiDao + .lookupUserIdByName(moderatorNameToRemove) + .orElseThrow( + () -> + new IllegalArgumentException( + "Failed to find moderator by user name: " + moderatorNameToRemove)); + + Preconditions.checkState( + moderatorsDao.setRole(userId, UserRole.PLAYER) == 1, + "Failed to remove moderator status for: " + moderatorNameToRemove); + + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorIdRequesting) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_MODERATOR) + .actionTarget(moderatorNameToRemove) + .build()); + log.info(moderatorNameToRemove + " was removed from moderators"); + } + + /** Promotes a user to super-moderator. Can only be done by super moderators. */ + public void addAdmin(final int moderatorIdRequesting, final String username) { + final int userId = + userJdbiDao + .lookupUserIdByName(username) + .orElseThrow( + () -> new IllegalArgumentException("Failed to find user by name: " + username)); + + Preconditions.checkState( + moderatorsDao.setRole(userId, UserRole.ADMIN) == 1, + "Failed to add super moderator status for: " + username); + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(moderatorIdRequesting) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_SUPER_MOD) + .actionTarget(username) + .build()); + log.info(username + " was promoted to super mod"); + } + + /** Checks if any user exists in DB by the given name. */ + public boolean userExistsByName(final String username) { + return userJdbiDao.lookupUserIdByName(username).isPresent(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModule.java b/server/lobby-module/src/main/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModule.java new file mode 100644 index 0000000..279d70b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModule.java @@ -0,0 +1,43 @@ +package org.triplea.modules.moderation.remote.actions; + +import java.net.InetAddress; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao.AuditAction; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao.AuditArgs; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.http.client.web.socket.messages.envelopes.remote.actions.ShutdownServerMessage; +import org.triplea.web.socket.WebSocketMessagingBus; + +@Builder +public class RemoteActionsModule { + @Nonnull private final UserBanDao userBanDao; + @Nonnull private final ModeratorAuditHistoryDao auditHistoryDao; + @Nonnull private final WebSocketMessagingBus gameMessagingBus; + + public static RemoteActionsModule build( + final Jdbi jdbi, final WebSocketMessagingBus gameMessagingBus) { + return RemoteActionsModule.builder() + .auditHistoryDao(jdbi.onDemand(ModeratorAuditHistoryDao.class)) + .userBanDao(jdbi.onDemand(UserBanDao.class)) + .gameMessagingBus(gameMessagingBus) + .build(); + } + + public boolean isUserBanned(final InetAddress ip) { + return userBanDao.isBannedByIp(ip.getHostAddress()); + } + + public void addGameIdForShutdown(final int moderatorId, final String gameId) { + auditHistoryDao.addAuditRecord( + AuditArgs.builder() + .actionName(AuditAction.REMOTE_SHUTDOWN) + .actionTarget(gameId) + .moderatorUserId(moderatorId) + .build()); + + gameMessagingBus.broadcastMessage(new ShutdownServerMessage(gameId)); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/player/info/FetchPlayerInfoModule.java b/server/lobby-module/src/main/java/org/triplea/modules/player/info/FetchPlayerInfoModule.java new file mode 100644 index 0000000..812b821 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/player/info/FetchPlayerInfoModule.java @@ -0,0 +1,104 @@ +package org.triplea.modules.player.info; + +import java.util.Collection; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.player.info.PlayerAliasRecord; +import org.triplea.db.dao.moderator.player.info.PlayerBanRecord; +import org.triplea.db.dao.moderator.player.info.PlayerInfoForModeratorDao; +import org.triplea.db.dao.user.history.PlayerHistoryDao; +import org.triplea.db.dao.user.history.PlayerHistoryRecord; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.http.client.lobby.moderator.PlayerSummary; +import org.triplea.http.client.lobby.moderator.PlayerSummary.Alias; +import org.triplea.http.client.lobby.moderator.PlayerSummary.BanInformation; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.game.listing.GameListing; + +@AllArgsConstructor +public class FetchPlayerInfoModule { + private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + private final PlayerInfoForModeratorDao playerInfoForModeratorDao; + private final PlayerHistoryDao playerHistoryDao; + private final Chatters chatters; + private final GameListing gameListing; + + public static FetchPlayerInfoModule build( + final Jdbi jdbi, final Chatters chatters, final GameListing gameListing) { + return new FetchPlayerInfoModule( + PlayerApiKeyDaoWrapper.build(jdbi), + jdbi.onDemand(PlayerInfoForModeratorDao.class), + jdbi.onDemand(PlayerHistoryDao.class), + chatters, + gameListing); + } + + public PlayerSummary fetchPlayerInfoAsModerator(final PlayerChatId playerChatId) { + return fetchPlayerInfo(true, playerChatId); + } + + public PlayerSummary fetchPlayerInfo(final PlayerChatId playerChatId) { + return fetchPlayerInfo(false, playerChatId); + } + + private PlayerSummary fetchPlayerInfo( + final boolean isModerator, final PlayerChatId playerChatId) { + final var chatterSession = + chatters.lookupPlayerByChatId(playerChatId).orElseThrow(this::playerLeftChatException); + + var playerSummaryBuilder = + PlayerSummary.builder() + .registrationDateEpochMillis(lookupRegistrationDate(playerChatId)) + .currentGames( + gameListing.getGameNamesPlayerHasJoined( + chatterSession.getChatParticipant().getUserName())); + + // if a moderator is requesting player data, then attach ban and aliases information + if (isModerator) { + final PlayerIdentifiersByApiKeyLookup gamePlayerLookup = + apiKeyDaoWrapper + .lookupPlayerByChatId(playerChatId) + .orElseThrow(this::playerLeftChatException); + + playerSummaryBuilder = + playerSummaryBuilder + .systemId(gamePlayerLookup.getSystemId().getValue()) + .ip(chatterSession.getIp().toString()) + .aliases( + lookupPlayerAliases(gamePlayerLookup.getSystemId(), gamePlayerLookup.getIp())) + .bans(lookupPlayerBans(gamePlayerLookup.getSystemId(), gamePlayerLookup.getIp())); + } + + return playerSummaryBuilder.build(); + } + + @Nullable + private Long lookupRegistrationDate(final PlayerChatId playerChatId) { + return apiKeyDaoWrapper + .lookupUserIdByChatId(playerChatId) + .flatMap(playerHistoryDao::lookupPlayerHistoryByUserId) + .map(PlayerHistoryRecord::getRegistrationDate) + .orElse(null); + } + + private IllegalArgumentException playerLeftChatException() { + return new IllegalArgumentException("Player could not be found, have they left chat?"); + } + + private Collection lookupPlayerAliases(final SystemId systemId, final String ip) { + return playerInfoForModeratorDao.lookupPlayerAliasRecords(systemId.getValue(), ip).stream() + .map(PlayerAliasRecord::toAlias) + .collect(Collectors.toList()); + } + + private Collection lookupPlayerBans(final SystemId systemId, final String ip) { + return playerInfoForModeratorDao.lookupPlayerBanRecords(systemId.getValue(), ip).stream() + .map(PlayerBanRecord::toBanInformation) + .collect(Collectors.toList()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameIsAvailableValidation.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameIsAvailableValidation.java new file mode 100644 index 0000000..15026cc --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameIsAvailableValidation.java @@ -0,0 +1,27 @@ +package org.triplea.modules.user.account; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; + +@AllArgsConstructor(access = AccessLevel.PROTECTED, onConstructor_ = @VisibleForTesting) +public class NameIsAvailableValidation implements Function> { + + @Nonnull private final UserJdbiDao userJdbiDao; + + public static NameIsAvailableValidation build(final Jdbi jdbi) { + return new NameIsAvailableValidation(jdbi.onDemand(UserJdbiDao.class)); + } + + @Override + public Optional apply(final String playerName) { + return userJdbiDao.lookupUserIdByName(playerName.trim()).isPresent() + ? Optional.of("That name is already taken, please choose another") + : Optional.empty(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameValidation.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameValidation.java new file mode 100644 index 0000000..a9cf04c --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/NameValidation.java @@ -0,0 +1,41 @@ +package org.triplea.modules.user.account; + +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.moderator.BadWordsDao; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.username.ban.UsernameBanDao; +import org.triplea.domain.data.UserName; + +@Builder +public class NameValidation implements Function> { + + @Nonnull private final Function> syntaxValidation; + @Nonnull private final BadWordsDao badWordsDao; + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final UsernameBanDao usernameBanDao; + + public static NameValidation build(final Jdbi jdbi) { + return NameValidation.builder() + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .syntaxValidation(name -> Optional.ofNullable(UserName.validate(name))) + .badWordsDao(jdbi.onDemand(BadWordsDao.class)) + .usernameBanDao(jdbi.onDemand(UsernameBanDao.class)) + .build(); + } + + @Override + public Optional apply(final String playerName) { + return syntaxValidation + .apply(playerName) + .or( + () -> + badWordsDao.containsBadWord(playerName) + || usernameBanDao.nameIsBanned(playerName.trim()) + ? Optional.of("That is not a nice name") + : Optional.empty()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/PasswordBCrypter.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/PasswordBCrypter.java new file mode 100644 index 0000000..7305742 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/PasswordBCrypter.java @@ -0,0 +1,34 @@ +package org.triplea.modules.user.account; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import at.favre.lib.crypto.bcrypt.LongPasswordStrategies; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class PasswordBCrypter { + /** + * This is a helper method designed to simplify the bcrypt API and hide some of the constants + * involved. This method generates a hash with 10 rounds. This number is arbitrary and might + * increase at a later time. + * + * @param password The string to apply the bcrypt algorithm to. + * @return A hashed password using a randomly generated bcrypt salt. + */ + public static String hashPassword(final String password) { + return BCrypt.with(LongPasswordStrategies.none()).hashToString(10, password.toCharArray()); + } + + /** + * Checks of the provided password does match the existing hash. NOTE: Any passwords longer than + * 72 bytes (UTF-8) will result in the same hash as the version trimmed to 72 bytes. + * + * @param password The password to check. + * @param hash The hash to verify the password against. + * @return True if the password matches the hash, false otherwise. + */ + public static boolean verifyHash(final String password, final String hash) { + return BCrypt.verifyer(null, LongPasswordStrategies.none()) + .verify(password.toCharArray(), hash.toCharArray()) + .verified; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/AccountCreator.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/AccountCreator.java new file mode 100644 index 0000000..2fd619d --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/AccountCreator.java @@ -0,0 +1,39 @@ +package org.triplea.modules.user.account.create; + +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.http.client.lobby.login.CreateAccountResponse; +import org.triplea.java.Postconditions; +import org.triplea.modules.user.account.PasswordBCrypter; + +/** + * Responsible to execute a 'create account' request. We should already have validated the request + * and just need to store the new account in database. + */ +@Builder +class AccountCreator implements Function { + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final Function passwordEncryptor; + + public static AccountCreator build(final Jdbi jdbi) { + return AccountCreator.builder() + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .passwordEncryptor(PasswordBCrypter::hashPassword) + .build(); + } + + @Override + public CreateAccountResponse apply(final CreateAccountRequest createAccountRequest) { + final String cryptedPassword = passwordEncryptor.apply(createAccountRequest.getPassword()); + + final int rowCount = + userJdbiDao.createUser( + createAccountRequest.getUsername(), createAccountRequest.getEmail(), cryptedPassword); + Postconditions.assertState(rowCount == 1); + return CreateAccountResponse.SUCCESS_RESPONSE; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountModule.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountModule.java new file mode 100644 index 0000000..1cf4550 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountModule.java @@ -0,0 +1,40 @@ +package org.triplea.modules.user.account.create; + +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.http.client.lobby.login.CreateAccountResponse; + +/** + * Imperative shell for creating a user account. Validates a request, if valid, creates a new user + * account in database. + */ +@Builder +public class CreateAccountModule implements Function { + + @Nonnull private final Function> createAccountValidation; + @Nonnull private final Function accountCreator; + + public static CreateAccountModule build(final Jdbi jdbi) { + return CreateAccountModule.builder() + .accountCreator(AccountCreator.build(jdbi)) + .createAccountValidation(CreateAccountValidation.build(jdbi)) + .build(); + } + + @Override + public CreateAccountResponse apply(final CreateAccountRequest createAccountRequest) { + // create account if request is valid + return createAccountValidation + .apply(createAccountRequest) + .map(CreateAccountModule::createError) + .orElseGet(() -> accountCreator.apply(createAccountRequest)); + } + + private static CreateAccountResponse createError(final String errorMessage) { + return CreateAccountResponse.builder().errorMessage(errorMessage).build(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountValidation.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountValidation.java new file mode 100644 index 0000000..0a11b5c --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/CreateAccountValidation.java @@ -0,0 +1,47 @@ +package org.triplea.modules.user.account.create; + +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.modules.user.account.NameIsAvailableValidation; +import org.triplea.modules.user.account.NameValidation; + +/** + * Verifies that a new user account request is good-to-create. General validation rules are: + * + *

    + *
  • username is not already in database + *
  • username is not banned and does not contain a word from 'bad words list' + *
  • password is a min length + *
  • email looks valid, contains an '@' sign + *
+ */ +@Builder +class CreateAccountValidation implements Function> { + + @Nonnull private final Function> nameValidator; + @Nonnull private final Function> nameIsAvailableValidator; + @Nonnull private final Function> emailValidator; + @Nonnull private final Function> passwordValidator; + + public static CreateAccountValidation build(final Jdbi jdbi) { + return CreateAccountValidation.builder() + .nameValidator(NameValidation.build(jdbi)) + .emailValidator(new EmailValidation()) + .passwordValidator(new PasswordValidation()) + .nameIsAvailableValidator(NameIsAvailableValidation.build(jdbi)) + .build(); + } + + @Override + public Optional apply(final CreateAccountRequest createAccountRequest) { + return nameValidator + .apply(createAccountRequest.getUsername()) + .or(() -> nameIsAvailableValidator.apply(createAccountRequest.getUsername())) + .or(() -> emailValidator.apply(createAccountRequest.getEmail())) + .or(() -> passwordValidator.apply(createAccountRequest.getPassword())); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/EmailValidation.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/EmailValidation.java new file mode 100644 index 0000000..e105595 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/EmailValidation.java @@ -0,0 +1,15 @@ +package org.triplea.modules.user.account.create; + +import com.google.common.base.Strings; +import java.util.Optional; +import java.util.function.Function; +import org.triplea.domain.data.PlayerEmailValidation; + +public class EmailValidation implements Function> { + @Override + public Optional apply(final String email) { + return !Strings.isNullOrEmpty(email) && PlayerEmailValidation.isValid(email) + ? Optional.empty() + : Optional.of("Invalid email address"); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/PasswordValidation.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/PasswordValidation.java new file mode 100644 index 0000000..4e2e38b --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/create/PasswordValidation.java @@ -0,0 +1,22 @@ +package org.triplea.modules.user.account.create; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import java.util.Optional; +import java.util.function.Function; + +/** We expect password to be sent to server as a hash. Simply check that it is not too short. */ +public class PasswordValidation implements Function> { + /** + * Expected MIN_LENGTH is length of the shortest password a user can input on the client-side UI + * when creating a new account. + */ + @VisibleForTesting static final int MIN_LENGTH = 3; + + @Override + public Optional apply(final String password) { + return Strings.nullToEmpty(password).trim().length() < MIN_LENGTH + ? Optional.of("Password too short") + : Optional.empty(); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/AccessLogUpdater.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/AccessLogUpdater.java new file mode 100644 index 0000000..19e020e --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/AccessLogUpdater.java @@ -0,0 +1,31 @@ +package org.triplea.modules.user.account.login; + +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.access.log.AccessLogDao; +import org.triplea.java.Postconditions; + +@Builder +class AccessLogUpdater implements Consumer { + + @Nonnull private final AccessLogDao accessLogDao; + + public static AccessLogUpdater build(final Jdbi jdbi) { + return AccessLogUpdater.builder() // + .accessLogDao(jdbi.onDemand(AccessLogDao.class)) + .build(); + } + + @Override + public void accept(final LoginRecord loginRecord) { + final int updateCount = + accessLogDao.insertUserAccessRecord( + loginRecord.getUserName().getValue(), + loginRecord.getIp(), + loginRecord.getSystemId().getValue()); + + Postconditions.assertState(updateCount == 1); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/ApiKeyGenerator.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/ApiKeyGenerator.java new file mode 100644 index 0000000..59bac22 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/ApiKeyGenerator.java @@ -0,0 +1,36 @@ +package org.triplea.modules.user.account.login; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.domain.data.ApiKey; + +@Builder +class ApiKeyGenerator implements Function { + + @Nonnull private final PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + + public static ApiKeyGenerator build(final Jdbi jdbi) { + return ApiKeyGenerator.builder() // + .apiKeyDaoWrapper(PlayerApiKeyDaoWrapper.build(jdbi)) + .build(); + } + + @Override + public ApiKey apply(final LoginRecord loginRecord) { + try { + return apiKeyDaoWrapper.newKey( + loginRecord.getUserName(), + InetAddress.getByName(loginRecord.getIp()), + loginRecord.getSystemId(), + loginRecord.getPlayerChatId()); + } catch (final UnknownHostException e) { + throw new IllegalStateException( + "Unexpected exception for IP address: " + loginRecord.getIp(), e); + } + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LobbyLoginMessageDao.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LobbyLoginMessageDao.java new file mode 100644 index 0000000..ebc8242 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LobbyLoginMessageDao.java @@ -0,0 +1,26 @@ +package org.triplea.modules.user.account.login; + +import java.util.function.Supplier; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; + +/** Fetches from database the lobby login message */ +@AllArgsConstructor +public class LobbyLoginMessageDao implements Supplier { + + private final Jdbi jdbi; + + static LobbyLoginMessageDao build(final Jdbi jdbi) { + return new LobbyLoginMessageDao(jdbi); + } + + @Override + public String get() { + return jdbi.withHandle( + handle -> + handle + .createQuery("select message from lobby_message") // + .mapTo(String.class) + .one()); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginModule.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginModule.java new file mode 100644 index 0000000..40b5e40 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginModule.java @@ -0,0 +1,133 @@ +package org.triplea.modules.user.account.login; + +import com.google.common.base.Strings; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.http.client.lobby.login.LoginRequest; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.user.account.NameValidation; +import org.triplea.modules.user.account.login.authorizer.anonymous.AnonymousLogin; +import org.triplea.modules.user.account.login.authorizer.registered.PasswordCheck; +import org.triplea.modules.user.account.login.authorizer.temp.password.TempPasswordLogin; + +@Builder +public class LoginModule { + @Nonnull private final Predicate registeredLogin; + @Nonnull private final Predicate tempPasswordLogin; + @Nonnull private final Function> anonymousLogin; + @Nonnull private final Consumer accessLogUpdater; + @Nonnull private final Function apiKeyGenerator; + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final Function> nameValidation; + @Nonnull private final Supplier lobbyLoginMessageDao; + + public static LoginModule build(final Jdbi jdbi, final Chatters chatters) { + return LoginModule.builder() + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .accessLogUpdater(AccessLogUpdater.build(jdbi)) + .apiKeyGenerator(ApiKeyGenerator.build(jdbi)) + .anonymousLogin(AnonymousLogin.build(jdbi, chatters)) + .tempPasswordLogin(TempPasswordLogin.build(jdbi)) + .registeredLogin(PasswordCheck.build(jdbi)) + .nameValidation(NameValidation.build(jdbi)) + .lobbyLoginMessageDao(LobbyLoginMessageDao.build(jdbi)) + .build(); + } + + public LobbyLoginResponse doLogin( + final LoginRequest loginRequest, final String systemId, final String ip) { + + final Optional nameValidationError = nameValidation.apply(loginRequest.getName()); + if (nameValidationError.isPresent()) { + return LobbyLoginResponse.builder().failReason(nameValidationError.get()).build(); + } + + if (Strings.isNullOrEmpty(loginRequest.getName()) || Strings.isNullOrEmpty(systemId)) { + return LobbyLoginResponse.builder().failReason("Invalid login request").build(); + } + + final SystemId playerSystemId = SystemId.of(systemId); + final String nameValidation = UserName.validate(loginRequest.getName()); + if (nameValidation != null) { + return LobbyLoginResponse.builder().failReason("Invalid name: " + nameValidation).build(); + } + + final boolean hasPassword = !Strings.nullToEmpty(loginRequest.getPassword()).isEmpty(); + + // successful login case + if (hasPassword && registeredLogin.test(loginRequest)) { + final ApiKey apiKey = + recordLoginAndGenerateApiKey(loginRequest, playerSystemId, PlayerChatId.newId(), ip); + return LobbyLoginResponse.builder() + .apiKey(apiKey.getValue()) + .moderator(isModerator(loginRequest.getName())) + .lobbyMessage(lobbyLoginMessageDao.get()) + .build(); + // successful login using a temp password + } else if (hasPassword && tempPasswordLogin.test(loginRequest)) { + final ApiKey apiKey = + recordLoginAndGenerateApiKey(loginRequest, playerSystemId, PlayerChatId.newId(), ip); + return LobbyLoginResponse.builder() + .apiKey(apiKey.getValue()) + .passwordChangeRequired(true) + .moderator(isModerator(loginRequest.getName())) + .lobbyMessage(lobbyLoginMessageDao.get()) + .build(); + // bad password + } else if (hasPassword) { + return LobbyLoginResponse.builder() + .failReason("Invalid username and password combination") + .build(); + // no password -> anonymous login + } else { + final Optional errorMessage = + anonymousLogin.apply(UserName.of(loginRequest.getName())); + if (errorMessage.isPresent()) { + return LobbyLoginResponse.builder().failReason(errorMessage.get()).build(); + } else { + final ApiKey apiKey = + recordLoginAndGenerateApiKey(loginRequest, playerSystemId, PlayerChatId.newId(), ip); + return LobbyLoginResponse.builder() + .apiKey(apiKey.getValue()) + .lobbyMessage(lobbyLoginMessageDao.get()) + .build(); + } + } + } + + private ApiKey recordLoginAndGenerateApiKey( + final LoginRequest loginRequest, + final SystemId systemId, + final PlayerChatId playerchatId, + final String ip) { + final var loginRecord = + LoginRecord.builder() + .userName(UserName.of(loginRequest.getName())) + .systemId(systemId) + .playerChatId(playerchatId) + .ip(ip) + .build(); + accessLogUpdater.accept(loginRecord); + return apiKeyGenerator.apply(loginRecord); + } + + private boolean isModerator(final String username) { + return userJdbiDao + .lookupUserRoleByUserName(username) + .map(UserRole::isModerator) + .orElseThrow(() -> new AssertionError("Expected to find role for user: " + username)); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginRecord.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginRecord.java new file mode 100644 index 0000000..b9086ed --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/LoginRecord.java @@ -0,0 +1,17 @@ +package org.triplea.modules.user.account.login; + +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.Value; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; + +@Builder +@Value +public class LoginRecord { + @Nonnull private final UserName userName; + @Nonnull private String ip; + @Nonnull private final SystemId systemId; + @Nonnull private final PlayerChatId playerChatId; +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLogin.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLogin.java new file mode 100644 index 0000000..6dade52 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLogin.java @@ -0,0 +1,34 @@ +package org.triplea.modules.user.account.login.authorizer.anonymous; + +import com.google.common.base.Preconditions; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.domain.data.UserName; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.user.account.NameIsAvailableValidation; + +@Builder +public class AnonymousLogin implements Function> { + @Nonnull private final Chatters chatters; + @Nonnull private final Function> nameIsAvailableValidation; + + public static Function> build( + final Jdbi jdbi, final Chatters chatters) { + return AnonymousLogin.builder() + .chatters(chatters) + .nameIsAvailableValidation(NameIsAvailableValidation.build(jdbi)) + .build(); + } + + @Override + public Optional apply(final UserName userName) { + Preconditions.checkNotNull(userName); + return (!chatters.isPlayerConnected(userName) + && nameIsAvailableValidation.apply(userName.getValue()).isEmpty()) + ? Optional.empty() + : Optional.of("Name is already in use, please choose another"); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheck.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheck.java new file mode 100644 index 0000000..81a5ce3 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheck.java @@ -0,0 +1,37 @@ +package org.triplea.modules.user.account.login.authorizer.registered; + +import com.google.common.base.Preconditions; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.http.client.lobby.login.LoginRequest; +import org.triplea.java.ArgChecker; +import org.triplea.modules.user.account.PasswordBCrypter; + +@Builder +public class PasswordCheck implements Predicate { + + @Nonnull private final UserJdbiDao userJdbiDao; + @Nonnull private final BiPredicate passwordVerifier; + + public static PasswordCheck build(final Jdbi jdbi) { + return PasswordCheck.builder() + .passwordVerifier(PasswordBCrypter::verifyHash) + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .build(); + } + + @Override + public boolean test(final LoginRequest loginRequest) { + Preconditions.checkNotNull(loginRequest); + ArgChecker.checkNotEmpty(loginRequest.getName()); + ArgChecker.checkNotEmpty(loginRequest.getPassword()); + return userJdbiDao + .getPassword(loginRequest.getName()) + .map(dbPassword -> passwordVerifier.test(loginRequest.getPassword(), dbPassword)) + .orElse(false); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLogin.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLogin.java new file mode 100644 index 0000000..1b509f0 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLogin.java @@ -0,0 +1,44 @@ +package org.triplea.modules.user.account.login.authorizer.temp.password; + +import com.google.common.base.Preconditions; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.temp.password.TempPasswordDao; +import org.triplea.http.client.lobby.login.LoginRequest; +import org.triplea.modules.user.account.PasswordBCrypter; + +@Builder +public class TempPasswordLogin implements Predicate { + + @Nonnull private final TempPasswordDao tempPasswordDao; + @Nonnull private final BiPredicate passwordChecker; + + public static TempPasswordLogin build(final Jdbi jdbi) { + return TempPasswordLogin.builder() + .tempPasswordDao(jdbi.onDemand(TempPasswordDao.class)) + .passwordChecker(PasswordBCrypter::verifyHash) + .build(); + } + + @Override + public boolean test(final LoginRequest loginRequest) { + Preconditions.checkNotNull(loginRequest); + Preconditions.checkNotNull(loginRequest.getName()); + Preconditions.checkNotNull(loginRequest.getPassword()); + return tempPasswordDao + .fetchTempPassword(loginRequest.getName()) + .map( + tempPassword -> { + if (passwordChecker.test(loginRequest.getPassword(), tempPassword)) { + tempPasswordDao.invalidateTempPasswords(loginRequest.getName()); + return true; + } else { + return false; + } + }) + .orElse(false); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/modules/user/account/update/UpdateAccountService.java b/server/lobby-module/src/main/java/org/triplea/modules/user/account/update/UpdateAccountService.java new file mode 100644 index 0000000..3c2f7e1 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/modules/user/account/update/UpdateAccountService.java @@ -0,0 +1,39 @@ +package org.triplea.modules.user.account.update; + +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.Builder; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.modules.user.account.PasswordBCrypter; + +@Builder +public class UpdateAccountService { + + @Nonnull private final UserJdbiDao userJdbiDao; + + @Nonnull private final Function passwordEncrpter; + + public static UpdateAccountService build(final Jdbi jdbi) { + return UpdateAccountService.builder() + .userJdbiDao(jdbi.onDemand(UserJdbiDao.class)) + .passwordEncrpter(PasswordBCrypter::hashPassword) + .build(); + } + + public void changePassword(final int userId, final String newPassword) { + final int updateCount = userJdbiDao.updatePassword(userId, passwordEncrpter.apply(newPassword)); + assert updateCount == 1; + } + + public String fetchEmail(final int userId) { + final String email = userJdbiDao.fetchEmail(userId); + assert email != null; + return email; + } + + public void changeEmail(final int userId, final String newEmail) { + final int updateCount = userJdbiDao.updateEmail(userId, newEmail); + assert updateCount == 1; + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/web/socket/GameConnectionWebSocket.java b/server/lobby-module/src/main/java/org/triplea/web/socket/GameConnectionWebSocket.java new file mode 100644 index 0000000..77b0fe7 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/web/socket/GameConnectionWebSocket.java @@ -0,0 +1,33 @@ +package org.triplea.web.socket; + +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import org.triplea.http.client.web.socket.WebsocketPaths; + +@ServerEndpoint(WebsocketPaths.GAME_CONNECTIONS) +public class GameConnectionWebSocket { + @OnOpen + public void onOpen(final Session session) { + GenericWebSocket.getInstance(this.getClass()).onOpen(session); + } + + @OnMessage + public void onMessage(final Session session, final String message) { + GenericWebSocket.getInstance(this.getClass()).onMessage(session, message); + } + + @OnClose + public void onClose(final Session session, final CloseReason closeReason) { + GenericWebSocket.getInstance(this.getClass()).onClose(session, closeReason); + } + + @OnError + public void onError(final Session session, final Throwable throwable) { + GenericWebSocket.getInstance(this.getClass()).onError(session, throwable); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/web/socket/PlayerConnectionWebSocket.java b/server/lobby-module/src/main/java/org/triplea/web/socket/PlayerConnectionWebSocket.java new file mode 100644 index 0000000..2f59649 --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/web/socket/PlayerConnectionWebSocket.java @@ -0,0 +1,38 @@ +package org.triplea.web.socket; + +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import org.triplea.http.client.web.socket.WebsocketPaths; + +/** + * Handles chat connections. Largely delegates to {@see MessagingService}. A shared {@code + * MessagingService} is injected into each user session and is available from {@code Session} + * objects. + */ +@ServerEndpoint(WebsocketPaths.PLAYER_CONNECTIONS) +public class PlayerConnectionWebSocket { + @OnOpen + public void onOpen(final Session session) { + GenericWebSocket.getInstance(this.getClass()).onOpen(session); + } + + @OnMessage + public void onMessage(final Session session, final String message) { + GenericWebSocket.getInstance(this.getClass()).onMessage(session, message); + } + + @OnClose + public void onClose(final Session session, final CloseReason closeReason) { + GenericWebSocket.getInstance(this.getClass()).onClose(session, closeReason); + } + + @OnError + public void onError(final Session session, final Throwable throwable) { + GenericWebSocket.getInstance(this.getClass()).onError(session, throwable); + } +} diff --git a/server/lobby-module/src/main/java/org/triplea/web/socket/SessionBannedCheck.java b/server/lobby-module/src/main/java/org/triplea/web/socket/SessionBannedCheck.java new file mode 100644 index 0000000..485c6ab --- /dev/null +++ b/server/lobby-module/src/main/java/org/triplea/web/socket/SessionBannedCheck.java @@ -0,0 +1,24 @@ +package org.triplea.web.socket; + +import com.google.common.annotations.VisibleForTesting; +import java.net.InetAddress; +import java.util.function.Predicate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.jdbi.v3.core.Jdbi; +import org.triplea.db.dao.user.ban.UserBanDao; + +@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor_ = @VisibleForTesting) +public class SessionBannedCheck implements Predicate { + private final UserBanDao userBanDao; + + public static SessionBannedCheck build(final Jdbi jdbi) { + return new SessionBannedCheck(jdbi.onDemand(UserBanDao.class)); + } + + @Override + public boolean test(final InetAddress remoteAddress) { + final String ip = remoteAddress.getHostAddress(); + return userBanDao.isBannedByIp(ip); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/TestData.java b/server/lobby-module/src/test/java/org/triplea/TestData.java new file mode 100644 index 0000000..781d50b --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/TestData.java @@ -0,0 +1,25 @@ +package org.triplea; + +import java.time.Instant; +import lombok.experimental.UtilityClass; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.LobbyGame; + +@UtilityClass +public class TestData { + public static final ApiKey API_KEY = ApiKey.of("test"); + + public static final LobbyGame LOBBY_GAME = + LobbyGame.builder() + .hostAddress("127.0.0.1") + .hostPort(12) + .hostName("name") + .mapName("map") + .playerCount(3) + .gameRound(1) + .epochMilliTimeStarted(Instant.now().toEpochMilli()) + .passworded(false) + .status("Waiting For Players") + .comments("comments") + .build(); +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/LobbyModuleDatabaseTestSupport.java b/server/lobby-module/src/test/java/org/triplea/db/LobbyModuleDatabaseTestSupport.java new file mode 100644 index 0000000..e4fec0b --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/LobbyModuleDatabaseTestSupport.java @@ -0,0 +1,12 @@ +package org.triplea.db; + +import java.util.Collection; +import org.jdbi.v3.core.mapper.RowMapperFactory; +import org.triplea.spitfire.database.DatabaseTestSupport; + +public class LobbyModuleDatabaseTestSupport extends DatabaseTestSupport { + @Override + protected Collection rowMappers() { + return LobbyModuleRowMappers.rowMappers(); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/access/log/AccessLogDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/access/log/AccessLogDaoTest.java new file mode 100644 index 0000000..9f89cd6 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/access/log/AccessLogDaoTest.java @@ -0,0 +1,123 @@ +package org.triplea.db.dao.access.log; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class AccessLogDaoTest { + private static final String EMPTY_ACCESS_LOG = + "access_log/user_role.yml,access_log/lobby_user.yml"; + private static final String ACCESS_LOG_TABLES = + "access_log/user_role.yml,access_log/lobby_user.yml,access_log/access_log.yml"; + + private final AccessLogDao accessLogDao; + + @Test + @DataSet(cleanBefore = true, value = EMPTY_ACCESS_LOG, useSequenceFiltering = false) + void emptyDataCase() { + assertThat(accessLogDao.fetchAccessLogRows(0, 1, "%", "%", "%"), is(empty())); + } + + /** + * In this test we verify.: - records are returned in reverse chronological order - data values + * are as expected + */ + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void fetchTwoRows() { + List data = accessLogDao.fetchAccessLogRows(0, 1, "%", "%", "%"); + assertThat(data, hasSize(1)); + + verifyIsRowWithSecondUsername(data.get(0)); + + data = accessLogDao.fetchAccessLogRows(1, 1, "%", "%", "%"); + assertThat(data, hasSize(1)); + + verifyIsRowWithFirstUsername(data.get(0)); + } + + private void verifyIsRowWithSecondUsername(final AccessLogRecord accessLogRecord) { + assertThat(accessLogRecord.getAccessTime(), isInstant(2016, 1, 3, 23, 59, 20)); + assertThat(accessLogRecord.getIp(), is("127.0.0.2")); + assertThat(accessLogRecord.getSystemId(), is("system-id2")); + assertThat(accessLogRecord.getUsername(), is("second")); + assertThat(accessLogRecord.isRegistered(), is(false)); + } + + private void verifyIsRowWithFirstUsername(final AccessLogRecord accessLogRecord) { + assertThat(accessLogRecord.getAccessTime(), isInstant(2016, 1, 1, 23, 59, 20)); + assertThat(accessLogRecord.getIp(), is("127.0.0.1")); + assertThat(accessLogRecord.getSystemId(), is("system-id1")); + assertThat(accessLogRecord.getUsername(), is("first")); + assertThat(accessLogRecord.isRegistered(), is(true)); + } + + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void searchForAllIdentifiesWithExactMatch() { + final List data = + accessLogDao.fetchAccessLogRows(0, 2, "first", "127.0.0.1", "system-id1"); + + assertThat(data, hasSize(1)); + verifyIsRowWithFirstUsername(data.get(0)); + } + + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void searchForSystemId() { + final List data = + accessLogDao.fetchAccessLogRows(0, 2, "%", "%", "system-id1"); + + assertThat(data, hasSize(1)); + verifyIsRowWithFirstUsername(data.get(0)); + } + + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void searchForUserName() { + final List data = accessLogDao.fetchAccessLogRows(0, 2, "first", "%", "%"); + + assertThat(data, hasSize(1)); + verifyIsRowWithFirstUsername(data.get(0)); + } + + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void searchForIp() { + final List data = accessLogDao.fetchAccessLogRows(0, 2, "%", "127.0.0.1", "%"); + + assertThat(data, hasSize(1)); + verifyIsRowWithFirstUsername(data.get(0)); + } + + /** There are only 2 rows, requesting a row offset of '2' should yield no data. */ + @Test + @DataSet(value = ACCESS_LOG_TABLES, useSequenceFiltering = false) + void requestingRowsOffDataSetReturnsNothing() { + assertThat(accessLogDao.fetchAccessLogRows(2, 1, "%", "%", "%"), is(empty())); + } + + @Test + @DataSet(cleanBefore = true, value = EMPTY_ACCESS_LOG, useSequenceFiltering = false) + @ExpectedDataSet(value = "access_log/access_log_post_insert.yml", orderBy = "username") + void insertAccessLogRecords() { + accessLogDao.insertUserAccessRecord("anonymous", "127.0.0.50", "anonymous-system-id"); + accessLogDao.insertUserAccessRecord("registered_user", "127.0.0.20", "registered-system-id"); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoTest.java new file mode 100644 index 0000000..64851c9 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoTest.java @@ -0,0 +1,41 @@ +package org.triplea.db.dao.api.key; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class GameHostingApiKeyDaoTest { + + private final GameHostingApiKeyDao gameHostApiKeyDao; + + @Test + @DataSet(value = "game_hosting_api_key/key_exists.yml", useSequenceFiltering = false) + void keyExists() { + assertThat(gameHostApiKeyDao.keyExists("game-hosting-key"), is(true)); + } + + @Test + @DataSet(value = "game_hosting_api_key/key_exists.yml", useSequenceFiltering = false) + void keyDoesNotExist() { + assertThat(gameHostApiKeyDao.keyExists("DNE"), is(false)); + } + + @Test + @DataSet(value = "game_hosting_api_key/insert_key_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("game_hosting_api_key/insert_key_after.yml") + void insertKey() { + gameHostApiKeyDao.insertKey("game-hosting-api-key", "127.0.0.2"); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapperTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapperTest.java new file mode 100644 index 0000000..7de627c --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/GameHostingApiKeyDaoWrapperTest.java @@ -0,0 +1,62 @@ +package org.triplea.db.dao.api.key; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.ApiKey; +import org.triplea.java.IpAddressParser; + +@ExtendWith(MockitoExtension.class) +class GameHostingApiKeyDaoWrapperTest { + + @Mock private GameHostingApiKeyDao gameHostApiKeyDao; + + @Mock private Function keyHashingFunction; + + @Mock private Supplier keyMaker; + + @InjectMocks private GameHostingApiKeyDaoWrapper gameHostingApiKeyDaoWrapper; + + @Nested + class IsKeyValid { + @Test + void valid() { + when(keyHashingFunction.apply(ApiKey.of("valid-key"))).thenReturn("hashed-valid-key"); + when(gameHostApiKeyDao.keyExists("hashed-valid-key")).thenReturn(true); + + assertThat(gameHostingApiKeyDaoWrapper.isKeyValid(ApiKey.of("valid-key")), is(true)); + } + + @Test + void notValid() { + when(keyHashingFunction.apply(ApiKey.of("not-valid-key"))).thenReturn("hashed-not-valid-key"); + when(gameHostApiKeyDao.keyExists("hashed-not-valid-key")).thenReturn(false); + + assertThat(gameHostingApiKeyDaoWrapper.isKeyValid(ApiKey.of("not-valid-key")), is(false)); + } + } + + @Nested + class NewGameHostKey { + @Test + void verifyCreatingNewKey() { + when(keyMaker.get()).thenReturn(ApiKey.of("api-key")); + when(keyHashingFunction.apply(ApiKey.of("api-key"))).thenReturn("hashed-key"); + when(gameHostApiKeyDao.insertKey("hashed-key", "1.1.1.1")).thenReturn(1); + + final ApiKey result = + gameHostingApiKeyDaoWrapper.newGameHostKey(IpAddressParser.fromString("1.1.1.1")); + + assertThat(result, is(ApiKey.of("api-key"))); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoTest.java new file mode 100644 index 0000000..b44cc1e --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoTest.java @@ -0,0 +1,134 @@ +package org.triplea.db.dao.api.key; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.test.common.RequiresDatabase; + +@DataSet( + value = + "lobby_api_key/user_role.yml," + + "lobby_api_key/lobby_user.yml," + + "lobby_api_key/lobby_api_key.yml", + useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class PlayerApiKeyDaoTest { + + private static final int USER_ID = 50; + + private static final PlayerApiKeyLookupRecord EXPECTED_MODERATOR_DATA = + PlayerApiKeyLookupRecord.builder() + .userId(USER_ID) + .username("registered-user") + .userRole(UserRole.MODERATOR) + .playerChatId("chat-id0") + .apiKeyId(1000) + .build(); + private static final PlayerApiKeyLookupRecord EXPECTED_ANONYMOUS_DATA = + PlayerApiKeyLookupRecord.builder() + .username("some-other-name") + .userRole(UserRole.ANONYMOUS) + .playerChatId("chat-id1") + .apiKeyId(1001) + .build(); + + private final PlayerApiKeyDao playerApiKeyDao; + + @Test + void keyNotFound() { + assertThat(playerApiKeyDao.lookupByApiKey("key-does-not-exist"), isEmpty()); + } + + @Test + void registeredUser() { + final Optional result = playerApiKeyDao.lookupByApiKey("zapi-key1"); + + assertThat(result, isPresentAndIs(EXPECTED_MODERATOR_DATA)); + } + + @Test + void anonymousUser() { + final Optional result = playerApiKeyDao.lookupByApiKey("zapi-key2"); + + assertThat(result, isPresentAndIs(EXPECTED_ANONYMOUS_DATA)); + } + + @Test + @DataSet( + value = + "lobby_api_key/user_role.yml," + + "lobby_api_key/lobby_user.yml," + + "lobby_api_key/empty_lobby_api_key.yml", + useSequenceFiltering = false) + @ExpectedDataSet( + value = "lobby_api_key/lobby_api_key_post_insert.yml", + orderBy = "key", + ignoreCols = {"id", "date_created"}) + void storeKey() { + // name of the registered user and role_id do not have to strictly match what is in the + // lobby_user table, but we would expect it to match as we find user role id and user id by + // lookup from lobby_user table by username. + assertThat( + playerApiKeyDao.storeKey( + "registered-user-name", + 50, + 1, + "player-chat-id", + "registered-user-key", + "system-id", + "127.0.0.1"), + is(1)); + } + + @Test + @DataSet( + value = "lobby_api_key/user_role.yml, lobby_api_key/delete_old_keys_before.yml", + useSequenceFiltering = false) + @ExpectedDataSet(value = "lobby_api_key/delete_old_keys_after.yml", orderBy = "key") + void deleteOldKeys() { + playerApiKeyDao.deleteOldKeys(); + } + + @Test + void lookupByPlayerChatId() { + final Optional playerIdLookup = + playerApiKeyDao.lookupByPlayerChatId("chat-id0"); + + assertThat( + playerIdLookup, + isPresentAndIs( + PlayerIdentifiersByApiKeyLookup.builder() + .userName("registered-user") + .systemId("system-id0") + .ip("127.0.0.1") + .build())); + } + + @Test + void foundCase() { + final Optional result = playerApiKeyDao.lookupPlayerIdByPlayerChatId("chat-id0"); + + assertThat(result, isPresentAndIs(50)); + } + + @Test + void notFoundCase() { + final Optional result = playerApiKeyDao.lookupPlayerIdByPlayerChatId("DNE"); + + assertThat(result, isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapperTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapperTest.java new file mode 100644 index 0000000..6fe34dd --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyDaoWrapperTest.java @@ -0,0 +1,175 @@ +package org.triplea.db.dao.api.key; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsSame.sameInstance; +import static org.mockito.Mockito.when; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.db.dao.user.role.UserRoleDao; +import org.triplea.db.dao.user.role.UserRoleLookup; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; + +@ExtendWith(MockitoExtension.class) +class PlayerApiKeyDaoWrapperTest { + + private static final ApiKey API_KEY = ApiKey.of("api-key"); + private static final String HASHED_KEY = "Dead, rainy shores proud swashbuckler"; + private static final UserName PLAYER_NAME = UserName.of("The_captain"); + private static final PlayerChatId PLAYER_CHAT_ID = PlayerChatId.of("player-chat-id"); + private static final SystemId SYSTEM_ID = SystemId.of("system-id"); + private static final int ANONYMOUS_ROLE_ID = 123; + private static final PlayerIdentifiersByApiKeyLookup PLAYER_ID_LOOKUP = + PlayerIdentifiersByApiKeyLookup.builder() + .userName(PLAYER_NAME.getValue()) + .systemId(SYSTEM_ID.getValue()) + .ip("ip") + .build(); + + private static final UserRoleLookup USER_ROLE_LOOKUP = + UserRoleLookup.builder().userId(10).userRoleId(20).build(); + + private static final InetAddress IP; + + static { + try { + IP = InetAddress.getLocalHost(); + } catch (final UnknownHostException e) { + throw new IllegalStateException(e); + } + } + + @Mock private PlayerApiKeyDao lobbyApiKeyDao; + @Mock private UserJdbiDao userJdbiDao; + @Mock private UserRoleDao userRoleDao; + @Mock private Supplier keyMaker; + @Mock private Function keyHashingFunction; + + @InjectMocks private PlayerApiKeyDaoWrapper wrapper; + + @Mock private PlayerApiKeyLookupRecord apiKeyUserData; + + @Nested + class LookupByApiKey { + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Test + void foundCase() { + givenKeyLookupResult(Optional.of(apiKeyUserData)); + + final Optional result = wrapper.lookupByApiKey(API_KEY); + + assertThat(result, isPresent()); + assertThat(result.get(), sameInstance(apiKeyUserData)); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private void givenKeyLookupResult(final Optional dataResult) { + when(keyHashingFunction.apply(API_KEY)).thenReturn(HASHED_KEY); + when(lobbyApiKeyDao.lookupByApiKey(HASHED_KEY)).thenReturn(dataResult); + } + + @Test + void notFoundCase() { + givenKeyLookupResult(Optional.empty()); + + final Optional result = wrapper.lookupByApiKey(API_KEY); + + assertThat(result, isEmpty()); + } + } + + @Nested + class NewKey { + @Test + void anonymousUserNewKey() { + when(keyMaker.get()).thenReturn(API_KEY); + when(keyHashingFunction.apply(API_KEY)).thenReturn(HASHED_KEY); + when(userJdbiDao.lookupUserIdAndRoleIdByUserName(PLAYER_NAME.getValue())) + .thenReturn(Optional.empty()); + when(userRoleDao.lookupRoleId(UserRole.ANONYMOUS)).thenReturn(ANONYMOUS_ROLE_ID); + when(lobbyApiKeyDao.storeKey( + PLAYER_NAME.getValue(), + null, + ANONYMOUS_ROLE_ID, + PLAYER_CHAT_ID.getValue(), + HASHED_KEY, + SYSTEM_ID.getValue(), + IP.getHostAddress())) + .thenReturn(1); + + final ApiKey result = wrapper.newKey(PLAYER_NAME, IP, SYSTEM_ID, PLAYER_CHAT_ID); + + assertThat(result, is(API_KEY)); + } + + @Test + void registeredUserKey() { + when(keyMaker.get()).thenReturn(API_KEY); + when(keyHashingFunction.apply(API_KEY)).thenReturn(HASHED_KEY); + when(userJdbiDao.lookupUserIdAndRoleIdByUserName(PLAYER_NAME.getValue())) + .thenReturn(Optional.of(USER_ROLE_LOOKUP)); + when(lobbyApiKeyDao.storeKey( + PLAYER_NAME.getValue(), + USER_ROLE_LOOKUP.getUserId(), + USER_ROLE_LOOKUP.getUserRoleId(), + PLAYER_CHAT_ID.getValue(), + HASHED_KEY, + SYSTEM_ID.getValue(), + IP.getHostAddress())) + .thenReturn(1); + + final ApiKey result = wrapper.newKey(PLAYER_NAME, IP, SYSTEM_ID, PLAYER_CHAT_ID); + + assertThat(result, is(API_KEY)); + } + } + + @Test + void lookupByPlayerChatId() { + when(lobbyApiKeyDao.lookupByPlayerChatId(PLAYER_CHAT_ID.getValue())) + .thenReturn(Optional.of(PLAYER_ID_LOOKUP)); + + final Optional result = + wrapper.lookupPlayerByChatId(PLAYER_CHAT_ID); + + assertThat(result, isPresentAndIs(PLAYER_ID_LOOKUP)); + } + + @Test + void lookupUserByPlayerChatId() { + when(lobbyApiKeyDao.lookupPlayerIdByPlayerChatId(PLAYER_CHAT_ID.getValue())) + .thenReturn(Optional.of(123)); + + final Optional result = wrapper.lookupUserIdByChatId(PLAYER_CHAT_ID); + + assertThat(result, isPresentAndIs(123)); + } + + @Test + void lookupUserByPlayerChatIdNotFoundCase() { + when(lobbyApiKeyDao.lookupPlayerIdByPlayerChatId(PLAYER_CHAT_ID.getValue())) + .thenReturn(Optional.empty()); + + final Optional result = wrapper.lookupUserIdByChatId(PLAYER_CHAT_ID); + + assertThat(result, isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecordTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecordTest.java new file mode 100644 index 0000000..b563361 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/api/key/PlayerApiKeyLookupRecordTest.java @@ -0,0 +1,52 @@ +package org.triplea.db.dao.api.key; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.triplea.db.dao.user.role.UserRole; + +class PlayerApiKeyLookupRecordTest { + private static final PlayerApiKeyLookupRecord API_KEY_LOOKUP_RECORD = + PlayerApiKeyLookupRecord.builder() + .userId(1) + .apiKeyId(1) + .playerChatId("chat-id") + .userRole("role") + .username("user-name") + .build(); + + @Test + void zeroUserIdIsMappedToNull() { + final PlayerApiKeyLookupRecord result = + API_KEY_LOOKUP_RECORD.toBuilder().userRole(UserRole.ANONYMOUS).userId(0).build(); + + assertThat(result.getUserId(), nullValue()); + } + + @ParameterizedTest + @MethodSource + void invalidStates(final Supplier apiKeyLookupRecord) { + assertThrows(AssertionError.class, apiKeyLookupRecord::get); + } + + @SuppressWarnings("unused") + static List> invalidStates() { + return List.of( + // Non-Anonymous roles must have a user-id + () -> API_KEY_LOOKUP_RECORD.toBuilder().userId(0).userRole(UserRole.PLAYER).build(), + () -> API_KEY_LOOKUP_RECORD.toBuilder().userId(0).userRole(UserRole.MODERATOR).build(), + () -> API_KEY_LOOKUP_RECORD.toBuilder().userId(0).userRole(UserRole.ADMIN).build(), + + // Anonymous role may not have a user-id + () -> API_KEY_LOOKUP_RECORD.toBuilder().userId(1).userRole(UserRole.ANONYMOUS).build(), + + // Host role is not allowed (host API-keys are stored in a different table) + () -> API_KEY_LOOKUP_RECORD.toBuilder().userRole(UserRole.HOST).build()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDaoTest.java new file mode 100644 index 0000000..94414a3 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/chat/history/LobbyChatHistoryDaoTest.java @@ -0,0 +1,31 @@ +package org.triplea.db.dao.chat.history; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class LobbyChatHistoryDaoTest { + + private final LobbyChatHistoryDao lobbyChatHistoryDao; + + @Test + @DataSet( + value = + "lobby_chat_history/user_role.yml," + + "lobby_chat_history/lobby_user.yml," + + "lobby_chat_history/lobby_api_key.yml", + useSequenceFiltering = false) + @ExpectedDataSet("lobby_chat_history/lobby_chat_history_post_insert.yml") + void insertChatMessage() { + lobbyChatHistoryDao.insertMessage("username", 3000, "message"); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/lobby/games/LobbyGameDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/lobby/games/LobbyGameDaoTest.java new file mode 100644 index 0000000..1a2fcdf --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/lobby/games/LobbyGameDaoTest.java @@ -0,0 +1,48 @@ +package org.triplea.db.dao.lobby.games; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.TestData; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatMessageUpload; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class LobbyGameDaoTest { + private final LobbyGameDao lobbyGameDao; + + @Test + @DataSet(value = "lobby_games/game_hosting_api_key.yml", useSequenceFiltering = false) + @ExpectedDataSet("lobby_games/lobby_game_post_insert.yml") + void insertLobbyGame() { + lobbyGameDao.insertLobbyGame( + ApiKey.of("HOST"), + LobbyGameListing.builder() // + .gameId("game-id") + .lobbyGame(TestData.LOBBY_GAME) + .build()); + } + + @Test + @DataSet( + value = "lobby_games/game_hosting_api_key.yml, lobby_games/lobby_game.yml", + useSequenceFiltering = false) + @ExpectedDataSet("lobby_games/game_chat_history_post_insert.yml") + void insertChatMessage() { + lobbyGameDao.recordChat( + ChatMessageUpload.builder() + .gameId("gameid-100") + .fromPlayer("gameplayer") + .chatMessage("example message") + .build()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/BadWordsDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/BadWordsDaoTest.java new file mode 100644 index 0000000..2ccc6ca --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/BadWordsDaoTest.java @@ -0,0 +1,69 @@ +package org.triplea.db.dao.moderator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@DataSet(value = "bad_words/bad_word.yml", useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class BadWordsDaoTest { + private static final List expectedBadWords = List.of("aaa", "one", "two", "zzz"); + + private final BadWordsDao badWordsDao; + + @Test + void getBadWords() { + assertThat(badWordsDao.getBadWords(), is(expectedBadWords)); + } + + @Test + @ExpectedDataSet( + value = "bad_words/bad_word_post_insert.yml", + orderBy = {"word"}) + void addBadWord() { + assertThat(badWordsDao.addBadWord("new-bad-word"), is(1)); + } + + @Test + @ExpectedDataSet("bad_words/bad_word_post_remove.yml") + void removeBadWord() { + assertThat(badWordsDao.removeBadWord("not-present"), is(0)); + + expectedBadWords.forEach(badWord -> assertThat(badWordsDao.removeBadWord(badWord), is(1))); + } + + @SuppressWarnings("unused") + private static List badWordContains() { + final List badWords = new ArrayList<>(List.of("zzZz", "_two_")); + badWords.addAll(expectedBadWords); + return badWords; + } + + @ParameterizedTest + @MethodSource + void badWordContains(final String badWord) { + assertThat(badWordsDao.containsBadWord(badWord), is(true)); + } + + @ParameterizedTest + @ValueSource(strings = {"zz", "", "some string not containing any bad words"}) + void notBadWordContains(final String notInBadWords) { + assertThat(badWordsDao.containsBadWord(notInBadWords), is(false)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDaoTest.java new file mode 100644 index 0000000..09e30f0 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorAuditHistoryDaoTest.java @@ -0,0 +1,105 @@ +package org.triplea.db.dao.moderator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.jdbi.v3.core.statement.UnableToExecuteStatementException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@DataSet( + value = + "moderator_audit/user_role.yml," + + "moderator_audit/lobby_user.yml," + + "moderator_audit/moderator_action_history.yml", + useSequenceFiltering = false) +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class ModeratorAuditHistoryDaoTest { + + private static final int MODERATOR_ID = 900000; + private static final int MODERATOR_ID_DOES_NOT_EXIST = 1111; + + private final ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @Test + void addAuditRecordThrowsIfModeratorNameNotFound() { + assertThrows( + UnableToExecuteStatementException.class, + () -> + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID_DOES_NOT_EXIST) + .actionTarget("any-value") + .actionName(ModeratorAuditHistoryDao.AuditAction.BAN_USERNAME) + .build())); + } + + @Test + @DataSet( + value = + "moderator_audit/user_role.yml," + + "moderator_audit/lobby_user.yml," + + "moderator_audit/empty_moderator_action_history.yml", + useSequenceFiltering = false) + @ExpectedDataSet("moderator_audit/moderator_action_history_post_insert.yml") + void addAuditRecord() { + moderatorAuditHistoryDao.addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.BAN_USERNAME) + .actionTarget("ACTION_TARGET") + .build()); + } + + @Test + void selectHistory() { + List results = moderatorAuditHistoryDao.lookupHistoryItems(0, 3); + + assertThat(results, hasSize(3)); + + assertThat(results.get(0).getUsername(), is("moderator2")); + assertThat(results.get(0).getDateCreated(), isInstant(2016, 1, 5, 23, 59, 20)); + assertThat(results.get(0).getActionName(), is("BAN_USERNAME")); + assertThat(results.get(0).getActionTarget(), is("ACTION_TARGET5")); + + assertThat(results.get(1).getUsername(), is("moderator1")); + assertThat(results.get(1).getDateCreated(), isInstant(2016, 1, 4, 23, 59, 20)); + assertThat(results.get(1).getActionName(), is("BOOT_PLAYER")); + assertThat(results.get(1).getActionTarget(), is("ACTION_TARGET4")); + + assertThat(results.get(2).getUsername(), is("moderator2")); + assertThat(results.get(2).getDateCreated(), isInstant(2016, 1, 3, 23, 59, 20)); + assertThat(results.get(2).getActionName(), is("BAN_USERNAME")); + assertThat(results.get(2).getActionTarget(), is("ACTION_TARGET3")); + + // there are only 5 records total + results = moderatorAuditHistoryDao.lookupHistoryItems(3, 3); + assertThat(results, hasSize(2)); + assertThat(results.get(0).getUsername(), is("moderator2")); + assertThat(results.get(0).getDateCreated(), isInstant(2016, 1, 2, 23, 59, 20)); + assertThat(results.get(0).getActionName(), is("MUTE_USERNAME")); + assertThat(results.get(0).getActionTarget(), is("ACTION_TARGET2")); + + assertThat(results.get(1).getUsername(), is("moderator1")); + assertThat(results.get(1).getDateCreated(), isInstant(2016, 1, 1, 23, 59, 20)); + assertThat(results.get(1).getActionName(), is("BAN_USERNAME")); + assertThat(results.get(1).getActionTarget(), is("ACTION_TARGET1")); + + results = moderatorAuditHistoryDao.lookupHistoryItems(5, 3); + assertThat(results, is(empty())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorsDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorsDaoTest.java new file mode 100644 index 0000000..d85bef0 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/ModeratorsDaoTest.java @@ -0,0 +1,59 @@ +package org.triplea.db.dao.moderator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.test.common.RequiresDatabase; + +@DataSet( + value = "moderators/user_role.yml, moderators/lobby_user.yml, moderators/access_log.yml", + useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class ModeratorsDaoTest { + + private static final int NOT_MODERATOR_ID = 100000; + private static final int MODERATOR_ID = 900000; + private static final int SUPER_MODERATOR_ID = 900001; + + private final ModeratorsDao moderatorsDao; + + @Test + void getModerators() { + final List moderators = moderatorsDao.getModerators(); + + assertThat( + "User dataset contains three players: an admin, moderator, and a player. We " + + "expect the two non-player users to be returned.", + moderators, + hasSize(2)); + + assertThat(moderators.get(0).getUsername(), is("moderator")); + assertThat(moderators.get(0).getLastLogin(), isInstant(2001, 1, 1, 23, 59, 20)); + + assertThat(moderators.get(1).getUsername(), is("Super! moderator")); + assertThat(moderators.get(1).getLastLogin(), nullValue()); + } + + @Test + @ExpectedDataSet("moderators/lobby_user_post_update_roles.yml") + void updateRoles() { + assertThat(moderatorsDao.setRole(NOT_MODERATOR_ID, UserRole.MODERATOR), is(1)); + assertThat(moderatorsDao.setRole(MODERATOR_ID, UserRole.ADMIN), is(1)); + assertThat(moderatorsDao.setRole(SUPER_MODERATOR_ID, UserRole.PLAYER), is(1)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecordTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecordTest.java new file mode 100644 index 0000000..d2b9279 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/ChatHistoryRecordTest.java @@ -0,0 +1,23 @@ +package org.triplea.db.dao.moderator.chat.history; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.time.Instant; +import org.junit.jupiter.api.Test; + +class ChatHistoryRecordTest { + + @Test + void toChatHistoryMessage() { + final var chatHistoryRecord = + ChatHistoryRecord.builder().date(Instant.now()).message("message").username("user").build(); + + final var chatHistoryMessage = chatHistoryRecord.toChatHistoryMessage(); + + assertThat( + chatHistoryMessage.getEpochMilliDate(), is(chatHistoryRecord.getDate().toEpochMilli())); + assertThat(chatHistoryMessage.getUsername(), is(chatHistoryRecord.getUsername())); + assertThat(chatHistoryMessage.getMessage(), is(chatHistoryRecord.getMessage())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDaoTest.java new file mode 100644 index 0000000..1855849 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/chat/history/GameChatHistoryDaoTest.java @@ -0,0 +1,101 @@ +package org.triplea.db.dao.moderator.chat.history; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.hamcrest.collection.IsCollectionWithSize; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@DataSet( + value = + "game_chat_history/game_hosting_api_key.yml," + + "game_chat_history/lobby_game.yml," + + "game_chat_history/game_chat_history.yml", + useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class GameChatHistoryDaoTest { + private static final String SIR_HOSTS_A_LOT = "sir_hosts_a_lot"; + private static final String SIR_HOSTS_A_LITTLE = "sir_hosts_a_little"; + private static final String PLAYER1 = "player1"; + + private final GameChatHistoryDao gameChatHistoryDao; + + @Test + void gameDoesNotExist() { + assertThat(gameChatHistoryDao.getChatHistory("dne"), is(empty())); + } + + @Test + @DisplayName("Chat history for a game with no chat messages should be empty") + void emptyChatHistory() { + assertThat(gameChatHistoryDao.getChatHistory("game-empty-chat"), is(empty())); + } + + @Test + @DisplayName("Chat history does not select all chat messages, only recent past") + void oldChatMessagesAreFiltered() { + assertThat(gameChatHistoryDao.getChatHistory("game-far-past"), is(empty())); + } + + @Test + void viewChatHistoryForSirHostALotsGame() { + final List chats = gameChatHistoryDao.getChatHistory("game-hosts-a-lot"); + + assertThat(chats, IsCollectionWithSize.hasSize(4)); + + int i = 0; + assertThat(chats.get(i).getUsername(), is(PLAYER1)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 0, 20)); + assertThat(chats.get(i).getMessage(), is("Hello good sir")); + + i++; + assertThat(chats.get(i).getUsername(), is(SIR_HOSTS_A_LOT)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 1, 20)); + assertThat(chats.get(i).getMessage(), is("Why hello to you")); + + i++; + assertThat(chats.get(i).getUsername(), is(PLAYER1)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 2, 20)); + assertThat(chats.get(i).getMessage(), is("What a fine day it is my good sir")); + + i++; + assertThat(chats.get(i).getUsername(), is(SIR_HOSTS_A_LOT)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 3, 20)); + assertThat(chats.get(i).getMessage(), is("What a fine day it is indeed!")); + } + + @Test + void viewChatHistoryForSirHostALittlesGame() { + final List chats = gameChatHistoryDao.getChatHistory("game-hosts-a-little"); + + assertThat(chats, IsCollectionWithSize.hasSize(3)); + + int i = 0; + assertThat(chats.get(i).getUsername(), is(SIR_HOSTS_A_LITTLE)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 1, 20)); + assertThat(chats.get(i).getMessage(), is("hello!")); + + i++; + assertThat(chats.get(i).getUsername(), is(SIR_HOSTS_A_LOT)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 2, 20)); + assertThat(chats.get(i).getMessage(), is("join my game?")); + + i++; + assertThat(chats.get(i).getUsername(), is(SIR_HOSTS_A_LITTLE)); + assertThat(chats.get(i).getDate(), isInstant(2100, 1, 1, 23, 3, 20)); + assertThat(chats.get(i).getMessage(), is("Maybe another day")); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecordTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecordTest.java new file mode 100644 index 0000000..f58da87 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerAliasRecordTest.java @@ -0,0 +1,33 @@ +package org.triplea.db.dao.moderator.player.info; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.PlayerSummary.Alias; + +class PlayerAliasRecordTest { + private static final Instant DATE = + LocalDateTime.of(1990, 1, 1, 23, 59, 59) // + .toInstant(ZoneOffset.UTC); + + @Test + void toAlias() { + final Alias alias = + PlayerAliasRecord.builder() + .accessTime(DATE) + .ip("1.1.1.1") + .systemId("system-id") + .username("name") + .build() + .toAlias(); + + assertThat(alias.getEpochMilliDate(), is(DATE.toEpochMilli())); + assertThat(alias.getIp(), is("1.1.1.1")); + assertThat(alias.getName(), is("name")); + assertThat(alias.getSystemId(), is("system-id")); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecordTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecordTest.java new file mode 100644 index 0000000..743560d --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerBanRecordTest.java @@ -0,0 +1,37 @@ +package org.triplea.db.dao.moderator.player.info; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import org.junit.jupiter.api.Test; +import org.triplea.http.client.lobby.moderator.PlayerSummary.BanInformation; + +class PlayerBanRecordTest { + private static final Instant START_DATE = + LocalDateTime.of(1990, 1, 1, 23, 59, 59) // + .toInstant(ZoneOffset.UTC); + private static final Instant END_DATE = + LocalDateTime.of(2010, 1, 1, 23, 59, 59) // + .toInstant(ZoneOffset.UTC); + + @Test + void testToBanInformation() { + final BanInformation banInformation = + PlayerBanRecord.builder() + .banStart(START_DATE) + .banEnd(END_DATE) + .ip("1.1.1.1") + .systemId("system-id") + .username("name") + .build() + .toBanInformation(); + + assertThat(banInformation.getEpochMilliStartDate(), is(START_DATE.toEpochMilli())); + assertThat(banInformation.getEpochMillEndDate(), is(END_DATE.toEpochMilli())); + assertThat(banInformation.getIp(), is("1.1.1.1")); + assertThat(banInformation.getName(), is("name")); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDaoTest.java new file mode 100644 index 0000000..8e45e1f --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/moderator/player/info/PlayerInfoForModeratorDaoTest.java @@ -0,0 +1,159 @@ +package org.triplea.db.dao.moderator.player.info; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class PlayerInfoForModeratorDaoTest { + + private final PlayerInfoForModeratorDao playerInfoForModeratorDao; + + @Nested + @DataSet(value = "moderator_player_lookup/access_log.yml", useSequenceFiltering = false) + class LookupPlayerAliases { + @Test + void lookupEmptyCase() { + final List results = + playerInfoForModeratorDao.lookupPlayerAliasRecords("system-id-dne", "9.9.9.9"); + + assertThat(results, is(empty())); + } + + @Test + void lookupByBothSystemIdAndIp() { + final List results = + playerInfoForModeratorDao.lookupPlayerAliasRecords("system-id", "1.1.1.1"); + + assertThat( + "There are 6 records in the dataset, " + + "we expect 4 to match, and 2 of them to be de-duped by name" + + "with only the most recent de-duped record returned", + results, + hasSize(3)); + + assertThat(results.get(0).getUsername(), is("name3")); + assertThat(results.get(0).getIp(), is("2.2.2.2")); + assertThat(results.get(0).getSystemId(), is("system-id")); + assertThat(results.get(0).getDate(), isInstant(2154, 1, 1, 23, 59, 20)); + + assertThat(results.get(1).getUsername(), is("name2")); + assertThat(results.get(1).getIp(), is("1.1.1.1")); + assertThat(results.get(1).getSystemId(), is("system-id2")); + assertThat(results.get(1).getDate(), isInstant(2152, 1, 1, 23, 59, 20)); + + assertThat(results.get(2).getUsername(), is("name1")); + assertThat(results.get(2).getIp(), is("1.1.1.1")); + assertThat(results.get(2).getSystemId(), is("system-id")); + assertThat(results.get(2).getDate(), isInstant(2151, 1, 1, 23, 59, 20)); + } + + @Test + void lookupWithOnlyIpMatching() { + final List results = + playerInfoForModeratorDao.lookupPlayerAliasRecords("system-id-dne", "2.2.2.2"); + + assertThat("We expect to only match the one record with IP 2.2.2.2", results, hasSize(1)); + + assertThat(results.get(0).getUsername(), is("name3")); + assertThat(results.get(0).getIp(), is("2.2.2.2")); + assertThat(results.get(0).getSystemId(), is("system-id")); + assertThat(results.get(0).getDate(), isInstant(2154, 1, 1, 23, 59, 20)); + } + + @Test + void lookupWithOnlySystemIdMatching() { + final List results = + playerInfoForModeratorDao.lookupPlayerAliasRecords("system-id2", "9.9.9.9"); + + assertThat( + "We expect to only match the 1 record with system 'system-id2'", results, hasSize(1)); + + assertThat(results.get(0).getUsername(), is("name2")); + assertThat(results.get(0).getIp(), is("1.1.1.1")); + assertThat(results.get(0).getSystemId(), is("system-id2")); + assertThat(results.get(0).getDate(), isInstant(2152, 1, 1, 23, 59, 20)); + } + } + + @Nested + @DataSet(value = "moderator_player_lookup/banned_user.yml", useSequenceFiltering = false) + class LookupPlayerBans { + @Test + void emptyLookupCase() { + final List results = + playerInfoForModeratorDao.lookupPlayerBanRecords("system-id-dne", "9.9.9.9"); + + assertThat(results, is(empty())); + } + + @Test + void lookupByBothSystemIdAndIp() { + final List results = + playerInfoForModeratorDao.lookupPlayerBanRecords("system-id", "1.1.1.1"); + + assertThat(results, hasSize(3)); + + assertThat(results.get(0).getUsername(), is("name1")); + assertThat(results.get(0).getIp(), is("1.1.1.1")); + assertThat(results.get(0).getSystemId(), is("system-id")); + assertThat(results.get(0).getBanStart(), isInstant(2010, 1, 1, 23, 59, 20)); + assertThat(results.get(0).getBanEnd(), isInstant(2100, 1, 1, 23, 59, 20)); + + assertThat(results.get(1).getUsername(), is("name2")); + assertThat(results.get(1).getIp(), is("1.1.1.1")); + assertThat(results.get(1).getSystemId(), is("system-id2")); + assertThat(results.get(1).getBanStart(), isInstant(2000, 1, 1, 23, 59, 20)); + assertThat(results.get(1).getBanEnd(), isInstant(2050, 1, 1, 23, 59, 20)); + + assertThat(results.get(2).getUsername(), is("name2")); + assertThat(results.get(2).getIp(), is("2.2.2.2")); + assertThat(results.get(2).getSystemId(), is("system-id")); + assertThat(results.get(2).getBanStart(), isInstant(2000, 1, 1, 23, 59, 20)); + assertThat(results.get(2).getBanEnd(), isInstant(2010, 1, 1, 23, 59, 20)); + } + + @Test + void lookupWithMatchByIpOnly() { + final List results = + playerInfoForModeratorDao.lookupPlayerBanRecords("system-id-dne", "2.2.2.2"); + + assertThat(results, hasSize(1)); + + assertThat(results.get(0).getUsername(), is("name2")); + assertThat(results.get(0).getIp(), is("2.2.2.2")); + assertThat(results.get(0).getSystemId(), is("system-id")); + assertThat(results.get(0).getBanStart(), isInstant(2000, 1, 1, 23, 59, 20)); + assertThat(results.get(0).getBanEnd(), isInstant(2010, 1, 1, 23, 59, 20)); + } + + @Test + void lookupWithMatchBySystemIdOnly() { + final List results = + playerInfoForModeratorDao.lookupPlayerBanRecords("system-id2", "9.9.9.9"); + + assertThat(results, hasSize(1)); + + assertThat(results.get(0).getUsername(), is("name2")); + assertThat(results.get(0).getIp(), is("1.1.1.1")); + assertThat(results.get(0).getSystemId(), is("system-id2")); + assertThat(results.get(0).getBanStart(), isInstant(2000, 1, 1, 23, 59, 20)); + assertThat(results.get(0).getBanEnd(), isInstant(2050, 1, 1, 23, 59, 20)); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordDaoTest.java new file mode 100644 index 0000000..89b3841 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordDaoTest.java @@ -0,0 +1,91 @@ +package org.triplea.db.dao.temp.password; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@DataSet( + value = + "temp_password/user_role.yml," + + "temp_password/lobby_user.yml," + + "temp_password/temp_password_request.yml", + useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class TempPasswordDaoTest { + + private static final String USERNAME = "username"; + private static final String EMAIL = "email@"; + private static final int USER_ID = 500000; + + private static final String PASSWORD = "temp"; + private static final String NEW_PASSWORD = "new-temp"; + + private final TempPasswordDao tempPasswordDao; + + @Test + void fetchTempPassword() { + assertThat(tempPasswordDao.fetchTempPassword(USERNAME), isEmpty()); + assertThat(tempPasswordDao.fetchTempPassword("DNE"), isEmpty()); + tempPasswordDao.insertTempPassword(USERNAME, EMAIL, PASSWORD); + assertThat(tempPasswordDao.fetchTempPassword(USERNAME), isPresentAndIs(PASSWORD)); + } + + @Test + void lookupUserIdByUsernameAndEmail() { + assertThat( + tempPasswordDao.lookupUserIdByUsernameAndEmail(USERNAME, EMAIL), isPresentAndIs(USER_ID)); + assertThat(tempPasswordDao.lookupUserIdByUsernameAndEmail("DNE", "DNE"), isEmpty()); + } + + @Test + void lookupUserIdByUsername() { + assertThat(tempPasswordDao.lookupUserIdByUsername(USERNAME), isPresentAndIs(USER_ID)); + assertThat(tempPasswordDao.lookupUserIdByUsername("DNE"), isEmpty()); + } + + @Test + void insertPasswordReturnsFalseIfUserNotFound() { + assertThat(tempPasswordDao.insertTempPassword("DNE", "DNE", PASSWORD), is(false)); + } + + @Test + void insertTempPassword() { + assertThat(tempPasswordDao.insertTempPassword(USERNAME, EMAIL, NEW_PASSWORD), is(true)); + assertThat(tempPasswordDao.fetchTempPassword(USERNAME), isPresentAndIs(NEW_PASSWORD)); + } + + @Test + void invalidateTempPasswordsForMissingNameDoesNothing() { + // verify that before we do any invalidation that indeed our known user has a temp password + assertThat( + tempPasswordDao.fetchTempPassword("user-with-temp-password"), isPresentAndIs(PASSWORD)); + + // invalidate password for some other user + tempPasswordDao.invalidateTempPasswords("DNE"); + + // expect the temp password for the known user to still exist + assertThat( + tempPasswordDao.fetchTempPassword("user-with-temp-password"), isPresentAndIs(PASSWORD)); + } + + @Test + void invalidatePassword() { + tempPasswordDao.invalidateTempPasswords("user-with-temp-password"); + assertThat( + "With password invalidated, fetching temp password should return empty", + tempPasswordDao.fetchTempPassword("user-with-temp-password"), + isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDaoTest.java new file mode 100644 index 0000000..c4f52c5 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/temp/password/TempPasswordHistoryDaoTest.java @@ -0,0 +1,44 @@ +package org.triplea.db.dao.temp.password; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@DataSet( + cleanBefore = true, + value = "temp_password_history/sample.yml", + useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@RequiresDatabase +class TempPasswordHistoryDaoTest { + + private static final String USERNAME = "username"; + + private final TempPasswordHistoryDao tempPasswordHistoryDao; + + @Test + void verifyCountAndInsert() { + + final String localhost = "127.0.0.1"; + assertThat(tempPasswordHistoryDao.countRequestsFromAddress(localhost), is(0)); + + tempPasswordHistoryDao.recordTempPasswordRequest(localhost, USERNAME); + assertThat(tempPasswordHistoryDao.countRequestsFromAddress(localhost), is(1)); + + tempPasswordHistoryDao.recordTempPasswordRequest(localhost, USERNAME); + assertThat(tempPasswordHistoryDao.countRequestsFromAddress(localhost), is(2)); + + tempPasswordHistoryDao.recordTempPasswordRequest(localhost, "other-user"); + assertThat(tempPasswordHistoryDao.countRequestsFromAddress(localhost), is(3)); + + final String otherAddress = "127.0.0.2"; + assertThat(tempPasswordHistoryDao.countRequestsFromAddress(otherAddress), is(0)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/user/UserJdbiDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/user/UserJdbiDaoTest.java new file mode 100644 index 0000000..2afeaf7 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/user/UserJdbiDaoTest.java @@ -0,0 +1,87 @@ +package org.triplea.db.dao.user; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.db.dao.user.role.UserRoleLookup; +import org.triplea.test.common.RequiresDatabase; + +@DataSet(value = "user/user_role.yml,user/lobby_user.yml", useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class UserJdbiDaoTest { + + private static final int USER_ID = 900000; + private static final String USERNAME = "user"; + private static final String NEW_USERNAME = "new-user"; + + private static final String EMAIL = "user@email.com"; + private static final String PASSWORD = + "$2a$56789_123456789_123456789_123456789_123456789_123456789_"; + private static final String NEW_PASSWORD = + "$2a$abcde_123456789_123456789_123456789_123456789_123456789_"; + + private final UserJdbiDao userDao; + + @Test + void lookupUserIdByName() { + assertThat(userDao.lookupUserIdByName("DNE"), isEmpty()); + assertThat(userDao.lookupUserIdByName(USERNAME), isPresentAndIs(900000)); + } + + @Test + void getPassword() { + assertThat(userDao.getPassword(USERNAME), isPresentAndIs(PASSWORD)); + assertThat(userDao.getPassword("DNE"), isEmpty()); + } + + @ExpectedDataSet("user/change_password_after.yml") + @Test + void updatePassword() { + assertThat(userDao.updatePassword(USER_ID, NEW_PASSWORD), is(1)); + } + + @Test + void fetchEmail() { + assertThat(userDao.fetchEmail(USER_ID), is("email@")); + } + + @ExpectedDataSet("user/post_change_email.yml") + @Test + void updateEmail() { + userDao.updateEmail(USER_ID, "new-email@"); + } + + @DataSet(value = "user/user_role.yml,user/empty_lobby_user.yml", useSequenceFiltering = false) + @ExpectedDataSet(value = "user/create_user_after.yml", orderBy = "id", ignoreCols = "id") + @Test + void createUser() { + userDao.createUser(NEW_USERNAME, EMAIL, PASSWORD); + } + + @Test + void lookupUserRoleIdByName() { + assertThat(userDao.lookupUserIdAndRoleIdByUserName("does-not-exist"), isEmpty()); + assertThat( + userDao.lookupUserIdAndRoleIdByUserName(USERNAME), + isPresentAndIs(UserRoleLookup.builder().userId(USER_ID).userRoleId(1).build())); + } + + @Test + void lookupUserRoleByUserName() { + assertThat(userDao.lookupUserRoleByUserName("does-not-exist"), isEmpty()); + assertThat(userDao.lookupUserRoleByUserName(USERNAME), isPresentAndIs(UserRole.PLAYER)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/user/ban/UserBanDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/user/ban/UserBanDaoTest.java new file mode 100644 index 0000000..ecf2d16 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/user/ban/UserBanDaoTest.java @@ -0,0 +1,139 @@ +package org.triplea.db.dao.user.ban; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class UserBanDaoTest { + private final UserBanDao userBanDao; + + @Nested + @DataSet(value = "user_ban/banned_by_ip.yml", useSequenceFiltering = false) + class IsBannedByIp { + @Test + void isBannedByIpPositiveCase() { + assertThat(userBanDao.isBannedByIp("127.0.0.1"), is(true)); + } + + @Test + void notBannedWhenIpNotPresent() { + assertThat(userBanDao.isBannedByIp("1.1.1.1"), is(false)); + } + + @Test + void notBannedWhenBanIsExpired() { + assertThat(userBanDao.isBannedByIp("127.0.0.2"), is(false)); + } + } + + @Nested + @DataSet(value = "user_ban/lookup_bans.yml", useSequenceFiltering = false) + class BanLookups { + @Test + @DisplayName("Verify retrieval of all current bans") + void lookupBans() { + final List result = userBanDao.lookupBans(); + + assertThat(result, hasSize(2)); + assertThat(result.get(0).getBanExpiry(), isInstant(2100, 1, 1, 23, 59, 59)); + assertThat(result.get(0).getDateCreated(), isInstant(2020, 1, 1, 23, 59, 59)); + assertThat(result.get(0).getIp(), is("127.0.0.2")); + assertThat(result.get(0).getPublicBanId(), is("public-id2")); + assertThat(result.get(0).getSystemId(), is("system-id2")); + assertThat(result.get(0).getUsername(), is("username2")); + + assertThat(result.get(1).getBanExpiry(), isInstant(2200, 1, 1, 23, 59, 59)); + assertThat(result.get(1).getDateCreated(), isInstant(2010, 1, 1, 23, 59, 59)); + assertThat(result.get(1).getIp(), is("127.0.0.1")); + assertThat(result.get(1).getPublicBanId(), is("public-id1")); + assertThat(result.get(1).getSystemId(), is("system-id1")); + assertThat(result.get(1).getUsername(), is("username1")); + } + + @Test + @DisplayName("Verify ban lookup case with no results found") + void lookupBanEmptyCase() { + final Optional result = userBanDao.lookupBan("99.99.99.99", "system-id-DNE"); + + assertThat(result, isEmpty()); + } + + @Test + @DisplayName("Verify ban lookup by IP address") + void lookupBanRecordByIp() { + final Optional result = userBanDao.lookupBan("127.0.0.2", "any-system-id"); + assertThat( + result, + isPresentAndIs( + BanLookupRecord.builder() + .banExpiry(Instant.parse("2100-01-01T23:59:59.0Z")) + .publicBanId("public-id2") + .build())); + } + + @Test + @DisplayName("Verify we can lookup bans by system-id and will choose the row with max(expiry)") + void lookupBanRecordBySystemId() { + final Optional result = userBanDao.lookupBan("99.99.99.99", "system-id2"); + assertThat( + result, + isPresentAndIs( + BanLookupRecord.builder() + .banExpiry(Instant.parse("2100-01-01T23:59:59.0Z")) + .publicBanId("public-id2") + .build())); + } + } + + @Nested + @DataSet(value = "user_ban/lookup_username_by_ban_id.yml", useSequenceFiltering = false) + class LookupUsernameByBanId { + @Test + void banIdFound() { + assertThat(userBanDao.lookupUsernameByBanId("public-id"), isPresentAndIs("username")); + } + + @Test + void banIdNotFound() { + assertThat(userBanDao.lookupUsernameByBanId("DNE"), isEmpty()); + } + } + + @Nested + class AddAndRemoveBan { + @Test + @DataSet(value = "user_ban/remove_ban_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("user_ban/remove_ban_after.yml") + void removeBan() { + userBanDao.removeBan("public-id"); + } + + @Test + @DataSet(value = "user_ban/add_ban_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("user_ban/add_ban_after.yml") + void addBan() { + userBanDao.addBan("public-id", "username", "system-id", "127.0.0.3", 5); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleDaoTest.java new file mode 100644 index 0000000..5af20cd --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleDaoTest.java @@ -0,0 +1,32 @@ +package org.triplea.db.dao.user.role; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@DataSet(value = "user_role/initial.yml", useSequenceFiltering = false) +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class UserRoleDaoTest { + + private final UserRoleDao userRoleDao; + + @Test + void lookupAnonymousRoleId() { + assertThat(userRoleDao.lookupRoleId(UserRole.ANONYMOUS), is(1)); + } + + @Test + void lookupHostRoleId() { + assertThat(userRoleDao.lookupRoleId(UserRole.HOST), is(2)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleTest.java new file mode 100644 index 0000000..6f3e024 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/user/role/UserRoleTest.java @@ -0,0 +1,22 @@ +package org.triplea.db.dao.user.role; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UserRoleTest { + + @ParameterizedTest + @ValueSource(strings = {UserRole.MODERATOR, UserRole.ADMIN}) + void isModerator(final String role) { + assertThat(role + " is a moderator", UserRole.isModerator(role), is(true)); + } + + @ParameterizedTest + @ValueSource(strings = {UserRole.PLAYER, UserRole.HOST, UserRole.ANONYMOUS}) + void isNotModerator(final String role) { + assertThat(role + " is _not_ a moderator", UserRole.isModerator(role), is(false)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/db/dao/username/ban/UsernameBanDaoTest.java b/server/lobby-module/src/test/java/org/triplea/db/dao/username/ban/UsernameBanDaoTest.java new file mode 100644 index 0000000..65dd824 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/db/dao/username/ban/UsernameBanDaoTest.java @@ -0,0 +1,88 @@ +package org.triplea.db.dao.username.ban; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.triplea.test.common.IsInstant.isInstant; + +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; +import com.github.database.rider.junit5.DBUnitExtension; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class UsernameBanDaoTest { + + private final UsernameBanDao usernameBanDao; + + @Test + @DisplayName("Verify retrieving username bans") + @DataSet(value = "username_ban/get_banned_usernames.yml", useSequenceFiltering = false) + void getBannedUserNames() { + final List result = usernameBanDao.getBannedUserNames(); + assertThat(result, hasSize(2)); + + assertThat(result.get(0).getUsername(), is("USERNAME1")); + assertThat(result.get(0).getDateCreated(), isInstant(2001, 1, 1, 23, 59, 59)); + + assertThat(result.get(1).getUsername(), is("USERNAME2")); + assertThat(result.get(1).getDateCreated(), isInstant(2000, 1, 1, 23, 59, 59)); + } + + @Test + @DisplayName("Verify name matching") + @DataSet(value = "username_ban/get_banned_usernames.yml", useSequenceFiltering = false) + void nameIsBanned() { + assertThat( + "Exact match should return true", + usernameBanDao.nameIsBanned("username1"), // + is(true)); + + assertThat( + "Case insensitive match should return true", + usernameBanDao.nameIsBanned("username1".toUpperCase()), + is(true)); + + assertThat( + "Non-exact match should return false", + usernameBanDao.nameIsBanned("username1_"), // + is(false)); + } + + @Test + @DisplayName("Verify adding a username ban") + @DataSet(value = "username_ban/add_banned_username_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("username_ban/add_banned_username_after.yml") + void addBannedUserName() { + usernameBanDao.addBannedUserName("username"); + } + + @Test + @DisplayName("Verify removing a username ban") + @DataSet(value = "username_ban/remove_banned_username_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("username_ban/remove_banned_username_after.yml") + void removeBannedUserName() { + final int result = usernameBanDao.removeBannedUserName("username"); + + assertThat(result, is(1)); + } + + @Test + @DisplayName("Verify when removing a username that DNE, that nothing changes") + @DataSet(value = "username_ban/remove_banned_username_before.yml", useSequenceFiltering = false) + @ExpectedDataSet("username_ban/remove_banned_username_before.yml") + void removeBannedUserNameNameDoesNotExist() { + final int result = usernameBanDao.removeBannedUserName("DNE"); + + assertThat(result, is(0)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/ChattersTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/ChattersTest.java new file mode 100644 index 0000000..7a09c8f --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/ChattersTest.java @@ -0,0 +1,351 @@ +package org.triplea.modules.chat; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.triplea.java.DateTimeUtil.utcInstantOf; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import javax.websocket.CloseReason; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.web.socket.MessageEnvelope; +import org.triplea.java.IpAddressParser; +import org.triplea.web.socket.MessageBroadcaster; +import org.triplea.web.socket.WebSocketSession; + +@SuppressWarnings("InnerClassMayBeStatic") +@ExtendWith(MockitoExtension.class) +class ChattersTest { + + private static final ChatParticipant CHAT_PARTICIPANT = + ChatParticipant.builder().userName("player-name").playerChatId("333").build(); + private static final ChatParticipant CHAT_PARTICIPANT_2 = + ChatParticipant.builder().userName("player-name2").playerChatId("4444").build(); + + private final Chatters chatters = new Chatters(); + + @Mock private WebSocketSession session; + @Mock private WebSocketSession session2; + @Mock private MessageBroadcaster messageBroadcaster; + + @Test + void chattersIsInitiallyEmpty() { + assertThat(chatters.getChatters(), is(empty())); + } + + @Nested + class PlayerLookup { + @Test + void lookupPlayerBySessionEmptyCase() { + when(session2.getId()).thenReturn("session2-id"); + chatters.getParticipants().put("session-id", buildChatterSession(session)); + + assertThat( + "Searching for wrong session, session2 DNE", + chatters.lookupPlayerBySession(session2), + isEmpty()); + } + + @Test + void lookupPlayerBySession() { + when(session.getId()).thenReturn("session-id"); + final ChatterSession chatterSession = buildChatterSession(session); + chatters.getParticipants().put("session-id", buildChatterSession(session)); + + assertThat(chatters.lookupPlayerBySession(session), isPresentAndIs(chatterSession)); + } + + @Test + void lookupPlayerByChatIdEmptyCase() { + chatters.getParticipants().put("session-id", buildChatterSession(session)); + + assertThat( + chatters.lookupPlayerByChatId(PlayerChatId.of("DNE")), // + isEmpty()); + } + + @Test + void lookupPlayerByChatId() { + final ChatterSession chatterSession = buildChatterSession(session); + chatters.getParticipants().put("session-id", buildChatterSession(session)); + + assertThat( + chatters.lookupPlayerByChatId(chatterSession.getChatParticipant().getPlayerChatId()), + isPresentAndIs(chatterSession)); + } + } + + @Nested + class IsPlayerConnected { + @Test + void hasPlayerReturnsFalseWithNoChatters() { + assertThat(chatters.isPlayerConnected(CHAT_PARTICIPANT.getUserName()), is(false)); + assertThat(chatters.isPlayerConnected(CHAT_PARTICIPANT_2.getUserName()), is(false)); + } + + @Test + void hasPlayerPlayerReturnsTrueIfNameMatches() { + when(session.getId()).thenReturn("session-id"); + chatters.connectPlayer(buildChatterSession(session)); + + // one chatter is added + assertThat(chatters.isPlayerConnected(CHAT_PARTICIPANT.getUserName()), is(true)); + assertThat(chatters.isPlayerConnected(CHAT_PARTICIPANT_2.getUserName()), is(false)); + } + } + + @Nested + class FetchAnyOpenSession { + @Test + void noSessions() { + assertThat(chatters.fetchOpenSessions(), empty()); + } + + @Test + void fetchSession() { + when(session.isOpen()).thenReturn(true); + when(session.getId()).thenReturn("9"); + + chatters.connectPlayer(buildChatterSession(session)); + assertThat(chatters.fetchOpenSessions(), hasItems(session)); + } + + @Test + void fetchOnlyOpenSessions() { + when(session.isOpen()).thenReturn(true); + when(session.getId()).thenReturn("30"); + when(session2.isOpen()).thenReturn(false); + when(session2.getId()).thenReturn("31"); + + chatters.connectPlayer(buildChatterSession(session)); + chatters.connectPlayer(buildChatterSession(session2)); + + assertThat(chatters.fetchOpenSessions(), hasItems(session)); + } + + @Test + void fetchMultipleOpenSession() { + when(session.isOpen()).thenReturn(true); + when(session.getId()).thenReturn("90"); + + when(session2.isOpen()).thenReturn(true); + when(session2.getId()).thenReturn("91"); + + chatters.connectPlayer(buildChatterSession(session)); + chatters.connectPlayer(buildChatterSession(session2)); + + assertThat(chatters.fetchOpenSessions(), hasItems(session, session2)); + } + } + + private ChatterSession buildChatterSession(final WebSocketSession session) { + return ChatterSession.builder() + .session(session) + .chatParticipant(CHAT_PARTICIPANT) + .apiKeyId(123) + .ip(IpAddressParser.fromString("1.1.1.1")) + .build(); + } + + @Nested + class DisconnectPlayerByName { + @Test + void noOpIfPlayerNotConnected() { + final boolean result = + chatters.disconnectPlayerByName(CHAT_PARTICIPANT.getUserName(), "disconnect message"); + assertThat(result, is(false)); + } + + @Test + void singleSessionDisconnected() { + when(session.getId()).thenReturn("100"); + chatters.connectPlayer(buildChatterSession(session)); + + final boolean result = + chatters.disconnectPlayerByName(CHAT_PARTICIPANT.getUserName(), "disconnect message"); + assertThat(result, is(true)); + + verify(session).close(any(CloseReason.class)); + } + + @Test + @DisplayName("Players can have multiple sessions, verify they are all closed") + void allSameNamePlayersAreDisconnected() { + when(session.getId()).thenReturn("1"); + when(session2.getId()).thenReturn("2"); + + chatters.connectPlayer(buildChatterSession(session)); + chatters.connectPlayer(buildChatterSession(session2)); + + final boolean result = + chatters.disconnectPlayerByName(CHAT_PARTICIPANT.getUserName(), "disconnect message"); + assertThat(result, is(true)); + + verify(session).close(any(CloseReason.class)); + verify(session2).close(any(CloseReason.class)); + } + } + + @Nested + class DisconnectPlayerByIp { + @Test + void noOpIfPlayerNotConnected() { + final boolean result = + chatters.disconnectIp(IpAddressParser.fromString("1.1.1.1"), "disconnect message"); + assertThat(result, is(false)); + } + + @Test + void singleSessionDisconnected() { + when(session.getId()).thenReturn("100"); + final ChatterSession chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + + final boolean result = chatters.disconnectIp(chatterSession.getIp(), "disconnect message"); + assertThat(result, is(true)); + + verify(session).close(any(CloseReason.class)); + } + + @Test + @DisplayName("Players can have multiple sessions, verify they are all closed") + void allSameIpsAreDisconnected() { + when(session.getId()).thenReturn("1"); + when(session2.getId()).thenReturn("2"); + + final ChatterSession session1 = buildChatterSession(session); + chatters.connectPlayer(session1); + chatters.connectPlayer(buildChatterSession(session2)); + + final boolean result = chatters.disconnectIp(session1.getIp(), "disconnect message"); + assertThat(result, is(true)); + + verify(session).close(any(CloseReason.class)); + verify(session2).close(any(CloseReason.class)); + } + } + + @Nested + class Muting { + private final Instant now = utcInstantOf(2000, 1, 1, 12, 20); + + @Test + @DisplayName("With no players connected, checking if a player is muted is trivially false") + void playerIsNotConnected() { + assertThat( + chatters.getPlayerMuteExpiration(IpAddressParser.fromString("1.1.1.1")), isEmpty()); + } + + @Test + @DisplayName("A player is connected, but not muted") + void playerIsNotMuted() { + when(session.getId()).thenReturn("session-id"); + final var chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + + final Optional result = + chatters.getPlayerMuteExpiration(IpAddressParser.fromString("55.55.55.55")); + + assertThat(result, isEmpty()); + } + + @Test + @DisplayName("A player is connected, was muted, but mute has expired") + void playerMuteIsExpired() { + when(session.getId()).thenReturn("session-id"); + final var chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + chatters.mutePlayer( + chatterSession.getChatParticipant().getPlayerChatId(), + 20, + Clock.fixed(now, ZoneOffset.UTC), + messageBroadcaster); + + final Optional result = + chatters.getPlayerMuteExpiration( + chatterSession.getIp(), + Clock.fixed(now.plus(21, ChronoUnit.MINUTES), ZoneOffset.UTC)); + + assertThat("Current time is *after* mute expiry => not muted", result, isEmpty()); + } + + @Test + void playerIsMuted() { + when(session.getId()).thenReturn("session-id"); + final var chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + chatters.mutePlayer( + chatterSession.getChatParticipant().getPlayerChatId(), + 1, + Clock.fixed(now, ZoneOffset.UTC), + messageBroadcaster); + + final Optional result = + chatters.getPlayerMuteExpiration( + chatterSession.getIp(), Clock.fixed(now, ZoneOffset.UTC)); + + assertThat( + "Current time is *before* mute expiry => muted", + result, + isPresentAndIs(now.plus(1, ChronoUnit.MINUTES))); + } + + @Test + @DisplayName("Check that an expired mute is removed") + void expiredPlayerMutesAreExpunged() { + when(session.getId()).thenReturn("session-id"); + final var chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + chatters.mutePlayer( + chatterSession.getChatParticipant().getPlayerChatId(), + 20, + Clock.fixed(now, ZoneOffset.UTC), + messageBroadcaster); + + // current time is after the mute expiry, we expect the mute to be expunged + chatters.getPlayerMuteExpiration( + chatterSession.getIp(), Clock.fixed(now.plus(21, ChronoUnit.MINUTES), ZoneOffset.UTC)); + + assertThat( + "Querying for the mute again, this time with current time before the mute." + + "Normally this would be a condition for a mute, but we expunged the mute." + + "Given the mute is expunged, we expect an empty result.", + chatters.getPlayerMuteExpiration( + chatterSession.getIp(), Clock.fixed(now, ZoneOffset.UTC)), + isEmpty()); + } + + @Test + @DisplayName("Verify that we broadcast a 'player-was-muted' message to all players when muting") + void playerMuteActionIsBroadcasted() { + when(session.getId()).thenReturn("session-id"); + final var chatterSession = buildChatterSession(session); + chatters.connectPlayer(chatterSession); + + chatters.mutePlayer( + chatterSession.getChatParticipant().getPlayerChatId(), + 20, + Clock.systemUTC(), + messageBroadcaster); + + verify(messageBroadcaster).accept(any(), any(MessageEnvelope.class)); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatMessageListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatMessageListenerTest.java new file mode 100644 index 0000000..17292e6 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatMessageListenerTest.java @@ -0,0 +1,113 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.chat.history.LobbyChatHistoryDao; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatSentMessage; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class ChatMessageListenerTest { + + @Mock private Chatters chatters; + @Mock private LobbyChatHistoryDao lobbyChatHistoryDao; + @InjectMocks private ChatMessageListener chatMessageListener; + + @Mock private WebSocketSession session; + @Mock private WebSocketMessageContext messageContext; + + private final ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(ChatReceivedMessage.class); + + private ChatterSession chatterSession; + + @BeforeEach + void dataSetup() { + chatterSession = + ChatterSession.builder() + .session(session) + .chatParticipant( + ChatParticipant.builder() + .playerChatId(PlayerChatId.newId().getValue()) + .userName("user-name") + .build()) + .apiKeyId(123) + .ip(IpAddressParser.fromString("3.3.3.3")) + .build(); + } + + @Test + @DisplayName("If a player is not in the chatter session, then we do not relay their message") + void ifPlayerSessionDoesNotExistThenDoNotRelayTheirMessage() { + when(messageContext.getSenderSession()).thenReturn(session); + when(session.getRemoteAddress()).thenReturn(chatterSession.getIp()); + when(chatters.lookupPlayerBySession(session)).thenReturn(Optional.empty()); + + chatMessageListener.accept(messageContext); + + verify(messageContext, never()).broadcastMessage(any()); + verify(lobbyChatHistoryDao, never()).recordMessage(any(), anyInt()); + } + + @Test + @DisplayName("If a player is in the chatter session, then we do relay their message") + void ifPlayerSessionDoesExistThenRelayTheirMessage() { + when(messageContext.getSenderSession()).thenReturn(session); + when(messageContext.getMessage()).thenReturn(new ChatSentMessage("message")); + when(chatters.lookupPlayerBySession(session)).thenReturn(Optional.of(chatterSession)); + when(session.getRemoteAddress()).thenReturn(chatterSession.getIp()); + when(chatters.getPlayerMuteExpiration(chatterSession.getIp())).thenReturn(Optional.empty()); + + chatMessageListener.accept(messageContext); + + verify(messageContext).broadcastMessage(messageCaptor.capture()); + final ChatReceivedMessage chatReceivedMessage = messageCaptor.getValue(); + assertThat(chatReceivedMessage.getMessage(), is("message")); + assertThat( + chatReceivedMessage.getSender(), + is(UserName.of(chatterSession.getChatParticipant().getUserName().getValue()))); + verify(lobbyChatHistoryDao, timeout(1000)).recordMessage(chatReceivedMessage, 123); + } + + @Test + void mutedPlayerMessagesAreNotSent() { + when(messageContext.getSenderSession()).thenReturn(session); + when(chatters.lookupPlayerBySession(session)).thenReturn(Optional.of(chatterSession)); + when(session.getRemoteAddress()).thenReturn(chatterSession.getIp()); + when(chatters.getPlayerMuteExpiration(chatterSession.getIp())) + .thenReturn(Optional.of(Instant.now().plusSeconds(60))); + + chatMessageListener.accept(messageContext); + + // should not broadcast a muted players chat message + verify(messageContext, never()).broadcastMessage(any()); + // should send response to muted player that they are muted. + verify(messageContext).sendResponse(any(ChatEventReceivedMessage.class)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapterTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapterTest.java new file mode 100644 index 0000000..63ae968 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/ChatParticipantAdapterTest.java @@ -0,0 +1,89 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.PlayerApiKeyLookupRecord; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class ChatParticipantAdapterTest { + + private static final String USERNAME = "username-value"; + private final ChatParticipantAdapter chatParticipantAdapter = new ChatParticipantAdapter(); + + @Mock private WebSocketSession session; + + @Test + @DisplayName("Check data is copied from database lookup result to chatter session result object") + void verifyData() { + when(session.getRemoteAddress()).thenReturn(IpAddressParser.fromString("1.1.1.1")); + final var userWithRoleRecord = + PlayerApiKeyLookupRecord.builder() + .userId(20) + .apiKeyId(123) + .username(USERNAME) + .userRole(UserRole.PLAYER) + .playerChatId(PlayerChatId.newId().getValue()) + .build(); + + final ChatterSession result = chatParticipantAdapter.apply(session, userWithRoleRecord); + + assertThat(result.getSession(), is(session)); + assertThat(result.getApiKeyId(), is(123)); + assertThat(result.getChatParticipant().getPlayerChatId(), notNullValue()); + assertThat(result.getIp(), is(IpAddressParser.fromString("1.1.1.1"))); + } + + @ParameterizedTest + @ValueSource(strings = {UserRole.ADMIN, UserRole.MODERATOR}) + @DisplayName("Verify moderator flag is set for moderator user roles") + void moderatorUsers(final String moderatorUserRole) { + final var userWithRoleRecord = givenUserRecordWithRole(moderatorUserRole, 1); + when(session.getRemoteAddress()).thenReturn(IpAddressParser.fromString("1.1.1.1")); + + final ChatterSession result = chatParticipantAdapter.apply(session, userWithRoleRecord); + + assertThat(result.getChatParticipant().isModerator(), is(true)); + assertThat(result.getChatParticipant().getUserName().getValue(), is(USERNAME)); + } + + private PlayerApiKeyLookupRecord givenUserRecordWithRole( + final String userRole, final int userId) { + return PlayerApiKeyLookupRecord.builder() + .username(USERNAME) + .userRole(userRole) + .playerChatId(PlayerChatId.newId().getValue()) + .apiKeyId(123) + .userId(userId) + .build(); + } + + @ParameterizedTest + @CsvSource(value = {UserRole.ANONYMOUS + ",0", UserRole.PLAYER + ",1"}) + @DisplayName("Verify moderator flag is false for non-moderator roles") + void nonModeratorUsers(final String notModeratorUserRole, final String userId) { + final var userWithRoleRecord = + givenUserRecordWithRole(notModeratorUserRole, Integer.parseInt(userId)); + when(session.getRemoteAddress()).thenReturn(IpAddressParser.fromString("1.1.1.1")); + + final ChatterSession result = chatParticipantAdapter.apply(session, userWithRoleRecord); + + assertThat(result.getChatParticipant().isModerator(), is(false)); + assertThat(result.getChatParticipant().getUserName().getValue(), is(USERNAME)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerConnectedListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerConnectedListenerTest.java new file mode 100644 index 0000000..3acf944 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerConnectedListenerTest.java @@ -0,0 +1,156 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerApiKeyLookupRecord; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.http.client.web.socket.MessageEnvelope; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatterListingMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ConnectToChatMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerJoinedMessage; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; +import org.triplea.web.socket.WebSocketMessagingBus; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class PlayerConnectedListenerTest { + + private static final ChatParticipant CHAT_PARTICIPANT = + ChatParticipant.builder() + .userName("user-name") + .isModerator(true) + .status("status") + .playerChatId("123") + .build(); + + @Mock private PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + @Mock private Chatters chatters; + + private PlayerConnectedListener playerConnectedListener; + + @Mock private WebSocketSession session; + @Mock private WebSocketMessageContext context; + @Mock private WebSocketMessagingBus webSocketMessagingBus; + private PlayerApiKeyLookupRecord apiKeyLookupRecord; + + private final ArgumentCaptor responseCaptor = + ArgumentCaptor.forClass(ChatterListingMessage.class); + private final ArgumentCaptor broadcastCaptor = + ArgumentCaptor.forClass(PlayerJoinedMessage.class); + + private ChatterSession chatterSession; + + @BeforeEach + void setupTestData() { + apiKeyLookupRecord = + PlayerApiKeyLookupRecord.builder() + .userRole("role") + .username(CHAT_PARTICIPANT.getUserName().getValue()) + .playerChatId("player-chat-id") + .apiKeyId(123) + .userId(33) + .build(); + context = + WebSocketMessageContext.builder() + .message(new ConnectToChatMessage(ApiKey.of("api-key"))) + .messagingBus(webSocketMessagingBus) + .senderSession(session) + .build(); + chatterSession = + ChatterSession.builder() + .apiKeyId(apiKeyLookupRecord.getApiKeyId()) + .chatParticipant(CHAT_PARTICIPANT) + .session(session) + .ip(IpAddressParser.fromString("5.5.5.5")) + .build(); + + playerConnectedListener = + PlayerConnectedListener.builder() + .apiKeyDaoWrapper(apiKeyDaoWrapper) + .chatParticipantAdapter(new ChatParticipantAdapter()) + .chatters(chatters) + .build(); + } + + @Test + @DisplayName( + "Verify no-op case where a user tries to connect directly to websocket" + + " without logging in to server first.") + void noOpIfApiKeyLookupReturnsEmpty() { + givenApiKeyLookupResult(null); + + playerConnectedListener.accept(context); + + verify(chatters, never()).connectPlayer(any()); + verify(webSocketMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + verify(webSocketMessagingBus, never()).sendResponse(any(), any()); + } + + private void givenApiKeyLookupResult( + @Nullable final PlayerApiKeyLookupRecord apiKeyLookupRecord) { + when(apiKeyDaoWrapper.lookupByApiKey(context.getMessage().getApiKey())) + .thenReturn(Optional.ofNullable(apiKeyLookupRecord)); + } + + @Test + @DisplayName("First time connections should send response and broadcast a player joined message") + void playerConnectsForFirstTime() { + givenApiKeyLookupResult(apiKeyLookupRecord); + + when(session.getRemoteAddress()).thenReturn(chatterSession.getIp()); + when(chatters.isPlayerConnected(chatterSession.getChatParticipant().getUserName())) + .thenReturn(false); + + playerConnectedListener.accept(context); + + verify(chatters).connectPlayer(chatterSession); + + verify(webSocketMessagingBus).sendResponse(eq(session), responseCaptor.capture()); + assertThat(responseCaptor.getValue().getChatters(), is(List.of())); + + verify(webSocketMessagingBus).broadcastMessage(broadcastCaptor.capture()); + assertThat(broadcastCaptor.getValue().getChatParticipant(), is(CHAT_PARTICIPANT)); + } + + @Test + @DisplayName("Registered users can have multiple connections and be listed under the same name") + void playerConnectsForSecondTime() { + givenApiKeyLookupResult(apiKeyLookupRecord); + + when(session.getRemoteAddress()).thenReturn(chatterSession.getIp()); + when(chatters.isPlayerConnected(chatterSession.getChatParticipant().getUserName())) + .thenReturn(true); + + playerConnectedListener.accept(context); + + verify(chatters).connectPlayer(chatterSession); + + verify(webSocketMessagingBus).sendResponse(eq(session), responseCaptor.capture()); + assertThat(responseCaptor.getValue().getChatters(), is(List.of())); + + // we do *not* broadcast a player joined message as the player appears already + // connected, the second connection is transparent to the other users. + verify(webSocketMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessageTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessageTest.java new file mode 100644 index 0000000..77eba72 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerIsMutedMessageTest.java @@ -0,0 +1,45 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.triplea.java.DateTimeUtil.utcInstantOf; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.function.Function; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PlayerIsMutedMessageTest { + private static final Instant CURRENT_TIME = utcInstantOf(2020, 11, 1, 2, 20); + + private final Function formattingFunction = + PlayerIsMutedMessage.MuteDurationRemainingCalculator.builder() + .clock(Clock.fixed(CURRENT_TIME, ZoneOffset.UTC)) + .build(); + + @Test + @DisplayName("Verify an example mute message calculation with 10 minutes remaining") + void verifyTimeDurationComputation() { + final Instant banExpiry = CURRENT_TIME.plus(10, ChronoUnit.MINUTES); + + final String result = formattingFunction.apply(banExpiry); + + assertThat(result, is("10 minutes")); + } + + @Test + @DisplayName("Verify an example mute message calculation with seconds remaining") + void verifyTimeDurationComputationWithSecondsRemaining() { + final Instant banExpiry = CURRENT_TIME.plus(20, ChronoUnit.SECONDS); + + final String result = formattingFunction.apply(banExpiry); + + assertThat(result, is("20 seconds")); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerLeftListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerLeftListenerTest.java new file mode 100644 index 0000000..2f67345 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/PlayerLeftListenerTest.java @@ -0,0 +1,67 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.MessageEnvelope; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerLeftMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class PlayerLeftListenerTest { + @Mock private Chatters chatters; + @InjectMocks private PlayerLeftListener playerLeftListener; + + @Mock private WebSocketSession session; + @Mock private WebSocketMessagingBus webSocketMessagingBus; + + private final ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(PlayerLeftMessage.class); + + @Test + void noopIfChattersSessionDoesNotExist() { + when(chatters.playerLeft(session)).thenReturn(Optional.empty()); + + playerLeftListener.accept(webSocketMessagingBus, session); + + verify(webSocketMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + } + + @Test + void playerDisconnect() { + when(chatters.playerLeft(session)).thenReturn(Optional.of(UserName.of("user-name"))); + + playerLeftListener.accept(webSocketMessagingBus, session); + + verify(webSocketMessagingBus).broadcastMessage(messageCaptor.capture()); + assertThat(messageCaptor.getValue().getUserName(), is(UserName.of("user-name"))); + } + + @Test + @DisplayName( + "If a registered player connects multiple times, only broadcast a 'player" + + "has left message' only when their last session has left.") + void playerDisconnectAndHasMultipleSessions() { + when(chatters.playerLeft(session)).thenReturn(Optional.of(UserName.of("user-name"))); + when(chatters.isPlayerConnected(UserName.of("user-name"))).thenReturn(true); + + playerLeftListener.accept(webSocketMessagingBus, session); + + verify(webSocketMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/SlapListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/SlapListenerTest.java new file mode 100644 index 0000000..d2c7d02 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/SlapListenerTest.java @@ -0,0 +1,84 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerSlapSentMessage; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class SlapListenerTest { + @Mock private Chatters chatters; + @InjectMocks private SlapListener slapListener; + + @Mock private WebSocketSession session; + @Mock private WebSocketMessageContext messageContext; + + private final ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(PlayerSlapReceivedMessage.class); + + @Test + void noopIfChattersSessionDoesNotExist() { + when(messageContext.getSenderSession()).thenReturn(session); + when(chatters.lookupPlayerBySession(session)).thenReturn(Optional.empty()); + + slapListener.accept(messageContext); + + verify(messageContext, never()).broadcastMessage(any()); + } + + @Test + @DisplayName("If a player is in the chatter session, then we do relay their message") + void ifPlayerSessionDoesExistThenRelayTheirMessage() { + when(messageContext.getSenderSession()).thenReturn(session); + when(messageContext.getMessage()) + .thenReturn(new PlayerSlapSentMessage(UserName.of("slapped-player"))); + givenChatterSession( + session, + ChatParticipant.builder() + .playerChatId(PlayerChatId.newId().getValue()) + .userName("user-name") + .build()); + + slapListener.accept(messageContext); + + verify(messageContext).broadcastMessage(messageCaptor.capture()); + verifyMessageContents(messageCaptor.getValue()); + } + + private void givenChatterSession( + final WebSocketSession session, final ChatParticipant chatParticipant) { + when(chatters.lookupPlayerBySession(session)) + .thenReturn( + Optional.of( + ChatterSession.builder() + .session(session) + .chatParticipant(chatParticipant) + .apiKeyId(123) + .build())); + } + + private static void verifyMessageContents(final PlayerSlapReceivedMessage chatReceivedMessage) { + assertThat(chatReceivedMessage.getSlappedPlayer(), is(UserName.of("slapped-player"))); + assertThat(chatReceivedMessage.getSlappingPlayer(), is(UserName.of("user-name"))); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/StatusUpdateListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/StatusUpdateListenerTest.java new file mode 100644 index 0000000..8be4704 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/chat/event/processing/StatusUpdateListenerTest.java @@ -0,0 +1,84 @@ +package org.triplea.modules.chat.event.processing; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateReceivedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.chat.PlayerStatusUpdateSentMessage; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessageContext; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class StatusUpdateListenerTest { + @Mock private Chatters chatters; + @InjectMocks private StatusUpdateListener statusUpdateListener; + + @Mock private WebSocketSession session; + @Mock private WebSocketMessageContext messageContext; + + private final ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(PlayerStatusUpdateReceivedMessage.class); + + @Test + void noopIfChattersSessionDoesNotExist() { + when(messageContext.getSenderSession()).thenReturn(session); + when(chatters.lookupPlayerBySession(session)).thenReturn(Optional.empty()); + + statusUpdateListener.accept(messageContext); + + verify(messageContext, never()).broadcastMessage(any()); + } + + @Test + @DisplayName("If a player is in the chatter session, then we do relay their message") + void ifPlayerSessionDoesExistThenRelayTheirMessage() { + when(messageContext.getSenderSession()).thenReturn(session); + when(messageContext.getMessage()).thenReturn(new PlayerStatusUpdateSentMessage("status")); + givenChatterSession( + session, + ChatParticipant.builder() + .playerChatId(PlayerChatId.newId().getValue()) + .userName("user-name") + .build()); + + statusUpdateListener.accept(messageContext); + + verify(messageContext).broadcastMessage(messageCaptor.capture()); + verifyMessageContents(messageCaptor.getValue()); + } + + private void givenChatterSession( + final WebSocketSession session, final ChatParticipant chatParticipant) { + when(chatters.lookupPlayerBySession(session)) + .thenReturn( + Optional.of( + ChatterSession.builder() + .session(session) + .chatParticipant(chatParticipant) + .apiKeyId(123) + .build())); + } + + private static void verifyMessageContents( + final PlayerStatusUpdateReceivedMessage chatReceivedMessage) { + assertThat(chatReceivedMessage.getStatus(), is("status")); + assertThat(chatReceivedMessage.getUserName(), is(UserName.of("user-name"))); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/ForgotPasswordModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/ForgotPasswordModuleTest.java new file mode 100644 index 0000000..4e14a62 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/ForgotPasswordModuleTest.java @@ -0,0 +1,102 @@ +package org.triplea.modules.forgot.password; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; + +@ExtendWith(MockitoExtension.class) +class ForgotPasswordModuleTest { + private static final String PASSWORD = "temp"; + private static final String IP_ADDRESS = "127.0.0.1"; + + private static final ForgotPasswordRequest forgotPasswordRequest = + ForgotPasswordRequest.builder().email("email").username("user").build(); + + @Mock private PasswordEmailSender passwordEmailSender; + @Mock private PasswordGenerator passwordGenerator; + @Mock private TempPasswordPersistence tempPasswordPersistence; + @Mock private TempPasswordHistory tempPasswordHistory; + + @InjectMocks private ForgotPasswordModule forgotPasswordModule; + + @Test + void verifyArgValidation() { + assertThrows( + IllegalArgumentException.class, + () -> forgotPasswordModule.apply(null, forgotPasswordRequest)); + assertThrows( + IllegalArgumentException.class, + () -> forgotPasswordModule.apply("", forgotPasswordRequest)); + assertThrows( + IllegalArgumentException.class, + () -> forgotPasswordModule.apply("not.an.ip.address", forgotPasswordRequest)); + assertThrows( + IllegalArgumentException.class, + () -> forgotPasswordModule.apply("127.0.0.1", ForgotPasswordRequest.builder().build())); + assertThrows( + IllegalArgumentException.class, + () -> + forgotPasswordModule.apply( + "127.0.0.1", + ForgotPasswordRequest.builder().username(" ").email("email@email.com").build())); + assertThrows( + IllegalArgumentException.class, + () -> + forgotPasswordModule.apply( + "127.0.0.1", + ForgotPasswordRequest.builder().username("username").email(" ").build())); + } + + @Test + void tooManyPasswordResetAttempts() { + when(tempPasswordHistory.allowRequestFromAddress(IP_ADDRESS)).thenReturn(false); + + assertThat( + forgotPasswordModule.apply(IP_ADDRESS, forgotPasswordRequest), + is(ForgotPasswordModule.ERROR_TOO_MANY_REQUESTS)); + + verify(tempPasswordHistory, never()).recordTempPasswordRequest(any(), any()); + verify(passwordGenerator, never()).generatePassword(); + verify(tempPasswordPersistence, never()).storeTempPassword(any(), any()); + verify(passwordEmailSender, never()).accept(any(), any()); + } + + @Test + void badEmailOrUser() { + when(tempPasswordHistory.allowRequestFromAddress(IP_ADDRESS)).thenReturn(true); + when(passwordGenerator.generatePassword()).thenReturn(PASSWORD); + when(tempPasswordPersistence.storeTempPassword(forgotPasswordRequest, PASSWORD)) + .thenReturn(false); + + assertThat( + forgotPasswordModule.apply(IP_ADDRESS, forgotPasswordRequest), + is(ForgotPasswordModule.ERROR_BAD_USER_OR_EMAIL)); + + verify(passwordEmailSender, never()).accept(any(), any()); + } + + @Test + void successCase() { + when(tempPasswordHistory.allowRequestFromAddress(IP_ADDRESS)).thenReturn(true); + when(passwordGenerator.generatePassword()).thenReturn(PASSWORD); + when(tempPasswordPersistence.storeTempPassword(forgotPasswordRequest, PASSWORD)) + .thenReturn(true); + + assertThat( + forgotPasswordModule.apply(IP_ADDRESS, forgotPasswordRequest), + is(ForgotPasswordModule.SUCCESS_MESSAGE)); + + verify(passwordEmailSender).accept(forgotPasswordRequest.getEmail(), PASSWORD); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/PasswordGeneratorTest.java b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/PasswordGeneratorTest.java new file mode 100644 index 0000000..c313460 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/PasswordGeneratorTest.java @@ -0,0 +1,37 @@ +package org.triplea.modules.forgot.password; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class PasswordGeneratorTest { + + @Test + void verifyPasswordLength() { + assertThat( + new PasswordGenerator().generatePassword().length(), is(PasswordGenerator.PASSWORD_LENGTH)); + } + + /** + * Generate a password, check we do not have it in a set, add the password to the set and repeat + * many times. + */ + @Test + void verifyPasswordChanges() { + final var passwordGenerator = new PasswordGenerator(); + + final Set strings = new HashSet<>(); + for (int i = 0; i < 1000; i++) { + final String generated = passwordGenerator.generatePassword(); + assertThat( + "If this test fails, DO NOT IGNORE IT! Instead increase the length of the generated " + + "temporary password", + strings.contains(generated), + is(false)); + strings.add(generated); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/TempPasswordPersistenceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/TempPasswordPersistenceTest.java new file mode 100644 index 0000000..7ba19d3 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/forgot/password/TempPasswordPersistenceTest.java @@ -0,0 +1,61 @@ +package org.triplea.modules.forgot.password; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.temp.password.TempPasswordDao; +import org.triplea.http.client.forgot.password.ForgotPasswordRequest; + +@ExtendWith(MockitoExtension.class) +class TempPasswordPersistenceTest { + private static final String USERNAME = "user"; + private static final String PASSWORD = "pass"; + private static final String HASHED_PASS = "hashed"; + private static final String BCRYPTED_PASS = "bcrypted"; + private static final String EMAIL = "email"; + + @Mock private TempPasswordDao tempPasswordDao; + @Mock private Function passwordHasher; + @Mock private Function hashedPasswordBcrypter; + + private TempPasswordPersistence tempPasswordPersistence; + + @BeforeEach + void setUp() { + tempPasswordPersistence = + new TempPasswordPersistence(tempPasswordDao, passwordHasher, hashedPasswordBcrypter); + } + + @Test + void storeTempPasswordUsernameNotFound() { + givenInsertResult(false); + + assertThat( + tempPasswordPersistence.storeTempPassword( + ForgotPasswordRequest.builder().username(USERNAME).email(EMAIL).build(), PASSWORD), + is(false)); + } + + private void givenInsertResult(final boolean result) { + when(passwordHasher.apply(PASSWORD)).thenReturn(HASHED_PASS); + when(hashedPasswordBcrypter.apply(HASHED_PASS)).thenReturn(BCRYPTED_PASS); + when(tempPasswordDao.insertTempPassword(USERNAME, EMAIL, BCRYPTED_PASS)).thenReturn(result); + } + + @Test + void storeTempPassword() { + givenInsertResult(true); + + assertThat( + tempPasswordPersistence.storeTempPassword( + ForgotPasswordRequest.builder().username(USERNAME).email(EMAIL).build(), PASSWORD), + is(true)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameListingTest.java b/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameListingTest.java new file mode 100644 index 0000000..19ae8c8 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameListingTest.java @@ -0,0 +1,453 @@ +package org.triplea.modules.game.listing; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.hamcrest.collection.IsMapWithSize.aMapWithSize; +import static org.hamcrest.collection.IsMapWithSize.anEmptyMap; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.text.IsEmptyString.emptyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.lobby.games.LobbyGameDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.LobbyGame; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.LobbyGameListing; +import org.triplea.http.client.web.socket.MessageEnvelope; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameUpdatedMessage; +import org.triplea.java.IpAddressParser; +import org.triplea.java.cache.ttl.ExpiringAfterWriteTtlCache; +import org.triplea.web.socket.WebSocketMessagingBus; + +/** + * Items to test.:
+ * - core functionality of get, add, remove, and keep-alive
+ * - games are added with an API key, keep-alive and remove should not do anything if API key is + * incorrect
+ * - 'dead-games' should be reaped on 'get'
+ * - boot-game, the authentication should be already done on controller level, so we just need to be + * sure there is bookkeeping. + */ +@ExtendWith(MockitoExtension.class) +class GameListingTest { + private static final String GAME_ID_0 = "id0"; + private static final String GAME_ID_1 = "id1"; + private static final String GAME_ID_2 = "id2"; + + private static final ApiKey API_KEY_0 = ApiKey.of("apiKey0"); + private static final ApiKey API_KEY_1 = ApiKey.of("apiKey1"); + + private static final GameListing.GameId ID_0 = new GameListing.GameId(API_KEY_0, GAME_ID_0); + private static final GameListing.GameId ID_1 = new GameListing.GameId(API_KEY_1, GAME_ID_1); + + private static final String HOST_NAME = "host-player"; + private static final int MODERATOR_ID = 33; + + private final ExpiringAfterWriteTtlCache cache = + new ExpiringAfterWriteTtlCache<>(1, TimeUnit.HOURS, (key, value) -> {}); + + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + @Mock private LobbyGameDao lobbyGameDao; + @Mock private WebSocketMessagingBus playerMessagingBus; + + private GameListing gameListing; + + @Mock private LobbyGame lobbyGame0; + @Mock private LobbyGame lobbyGame1; + @Mock private LobbyGame lobbyGame2; + + @BeforeEach + void setUp() { + gameListing = + GameListing.builder() + .playerMessagingBus(playerMessagingBus) + .auditHistoryDao(moderatorAuditHistoryDao) + .lobbyGameDao(lobbyGameDao) + .games(cache) + .build(); + } + + @Nested + final class GetGames { + + /** Basic case, no games added, expect none to be returned. */ + @Test + void getGames() { + cache.put(ID_0, lobbyGame0); + cache.put(new GameListing.GameId(API_KEY_0, GAME_ID_1), lobbyGame1); + cache.put(new GameListing.GameId(API_KEY_1, GAME_ID_2), lobbyGame2); + + final List result = gameListing.getGames(); + + assertThat( + result, + hasItem(LobbyGameListing.builder().gameId(GAME_ID_0).lobbyGame(lobbyGame0).build())); + + assertThat( + result, + hasItem(LobbyGameListing.builder().gameId(GAME_ID_1).lobbyGame(lobbyGame1).build())); + + assertThat( + result, + hasItem(LobbyGameListing.builder().gameId(GAME_ID_2).lobbyGame(lobbyGame2).build())); + } + } + + @Nested + final class KeepAlive { + @Test + void noGamesPresent() { + final boolean result = gameListing.keepAlive(API_KEY_0, GAME_ID_0); + assertThat("Game not found, keep alive should return false", result, is(false)); + + assertThat(cache.asMap(), is(anEmptyMap())); + } + + @Test + void gameExists() { + cache.put(ID_0, lobbyGame0); + + final boolean result = gameListing.keepAlive(API_KEY_0, GAME_ID_0); + + assertThat("Game found, keep alive should return true", result, is(true)); + assertThat(cache.asMap(), is(aMapWithSize(1))); + assertThat(cache.asMap(), hasEntry(ID_0, lobbyGame0)); + } + } + + @Nested + final class RemoveGame { + @Test + void removeGame() { + cache.put(new GameListing.GameId(API_KEY_0, GAME_ID_0), lobbyGame0); + + gameListing.removeGame(API_KEY_0, GAME_ID_0); + + assertThat(cache.asMap(), is(anEmptyMap())); + verify(playerMessagingBus).broadcastMessage(new LobbyGameRemovedMessage(GAME_ID_0)); + } + + @Test + void removeGameRequiresCorrectApiKey() { + cache.put(new GameListing.GameId(API_KEY_0, GAME_ID_0), lobbyGame0); + + gameListing.removeGame(API_KEY_1, GAME_ID_0); + + assertThat(cache.asMap(), is(aMapWithSize(1))); + assertThat(cache.asMap(), hasEntry(new GameListing.GameId(API_KEY_0, GAME_ID_0), lobbyGame0)); + verify(playerMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + } + } + + @Nested + final class PostGame { + @Test + void postGame() { + final String id0 = + gameListing.postGame( + API_KEY_0, + GamePostingRequest.builder().lobbyGame(lobbyGame0).playerNames(List.of()).build()); + + assertThat(id0, not(emptyString())); + assertThat(cache.asMap(), is(Map.of(new GameListing.GameId(API_KEY_0, id0), lobbyGame0))); + + final var lobbyGameListing = + LobbyGameListing.builder().gameId(id0).lobbyGame(lobbyGame0).build(); + verify(playerMessagingBus).broadcastMessage(new LobbyGameUpdatedMessage(lobbyGameListing)); + verify(lobbyGameDao).insertLobbyGame(API_KEY_0, lobbyGameListing); + } + } + + @Nested + final class UpdateGame { + @Test + void updateGameThatDoesNotExist() { + final boolean result = gameListing.updateGame(API_KEY_0, GAME_ID_0, lobbyGame0); + + assertThat(result, is(false)); + assertThat(cache.asMap(), is(anEmptyMap())); + verify(playerMessagingBus, never()).broadcastMessage(any(MessageEnvelope.class)); + } + + @Test + void updateGameThatDoesExist() { + cache.put(ID_0, lobbyGame1); + + final boolean result = gameListing.updateGame(API_KEY_0, GAME_ID_0, lobbyGame0); + + assertThat(result, is(true)); + + verify(playerMessagingBus) + .broadcastMessage( + new LobbyGameUpdatedMessage( + LobbyGameListing.builder().gameId(GAME_ID_0).lobbyGame(lobbyGame0).build())); + } + } + + @Nested + final class BootGame { + @Test + void bootGame() { + cache.put(ID_0, lobbyGame0); + when(lobbyGame0.getHostName()).thenReturn(HOST_NAME); + + gameListing.bootGame(MODERATOR_ID, GAME_ID_0); + + assertThat(cache.asMap(), is(anEmptyMap())); + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .actionName(ModeratorAuditHistoryDao.AuditAction.BOOT_GAME) + .actionTarget(HOST_NAME) + .moderatorUserId(MODERATOR_ID) + .build()); + verify(playerMessagingBus).broadcastMessage(new LobbyGameRemovedMessage(GAME_ID_0)); + } + } + + @Nested + final class IsValidGameIdApiKeyPair { + + @Test + void validCase() { + cache.put(ID_0, lobbyGame0); + final boolean result = gameListing.isValidApiKeyAndGameId(ID_0.getApiKey(), ID_0.getId()); + assertThat(result, is(true)); + } + + @Test + void mismatchOnApiKey() { + cache.put(ID_0, lobbyGame0); + final boolean result = + gameListing.isValidApiKeyAndGameId(ApiKey.of("incorrect-api-key"), ID_0.getId()); + assertThat(result, is(false)); + } + + @Test + void mismatchOnGameId() { + cache.put(ID_0, lobbyGame0); + final boolean result = + gameListing.isValidApiKeyAndGameId(ID_0.getApiKey(), "incorrect-game-id"); + assertThat(result, is(false)); + } + } + + @Nested + class GetHostForGame { + @Test + void doesNotExistCase() { + final Optional result = + gameListing.getHostForGame(ID_0.getApiKey(), "incorrect-game-id"); + + assertThat(result, isEmpty()); + } + + @Test + void badApiKeyCase() { + cache.put(ID_0, lobbyGame0); + + final Optional result = + gameListing.getHostForGame(ApiKey.of("incorrect"), ID_0.getId()); + + assertThat(result, isEmpty()); + } + + @Test + void badGameId() { + cache.put(ID_0, lobbyGame0); + + final Optional result = + gameListing.getHostForGame(ID_0.getApiKey(), "bad-game-id"); + + assertThat(result, isEmpty()); + } + + @Test + void happyCaseFindByApiKeyAndGameId() { + when(lobbyGame0.getHostAddress()).thenReturn("1.1.1.1"); + cache.put(ID_0, lobbyGame0); + + final Optional result = + gameListing.getHostForGame(ID_0.getApiKey(), ID_0.getId()); + + assertThat( + result, + isPresentAndIs( + new InetSocketAddress( + IpAddressParser.fromString("1.1.1.1"), lobbyGame0.getHostPort()))); + } + } + + @Nested + class PlayerIsInGameTracking { + @Test + void playerIsNotInAnyGamesEmptyCase() { + final UserName user = UserName.of("user"); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat(results, is(empty())); + } + + @Test + void addPlayerToSingleGame() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat(results, hasSize(1)); + } + + @Test + void addPlayerToSingleGameAndThenRemove() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + gameListing.removePlayerFromGame(user, ID_0.getApiKey(), ID_0.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat( + "player was added and then remove from a game, expect to no longer be in a game", + results, + is(empty())); + } + + @Test + void addPlayerToMultipleGame() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + cache.put(ID_1, lobbyGame1); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + gameListing.addPlayerToGame(user, ID_1.getApiKey(), ID_1.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat(results, hasSize(2)); + assertThat(results, hasItem(lobbyGame0.getHostName())); + assertThat(results, hasItem(lobbyGame1.getHostName())); + } + + @Test + void addPlayerToMultipleGameAndRemoveFromOneGame() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + cache.put(ID_1, lobbyGame1); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + gameListing.addPlayerToGame(user, ID_1.getApiKey(), ID_1.getId()); + gameListing.removePlayerFromGame(user, ID_0.getApiKey(), ID_0.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat(results, hasSize(1)); + assertThat( + "Player was removed from game0, should still be in game1", + results, + hasItem(lobbyGame1.getHostName())); + } + + @Test + @DisplayName( + "Games can expire, our tracking of player to games should take " + + "into account expired games") + void getPlayerInGamesOnlyReturnsCurrentGames() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + gameListing.addPlayerToGame(user, ID_1.getApiKey(), ID_1.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat("Player was added to two games, but only one game is alive", results, hasSize(1)); + assertThat(results, hasItem(lobbyGame0.getHostName())); + } + + @Test + void removingGamesWillExplicityRemoveParticipants() { + final UserName user = UserName.of("user"); + cache.put(ID_0, lobbyGame0); + gameListing.addPlayerToGame(user, ID_0.getApiKey(), ID_0.getId()); + gameListing.removeGame(ID_0.getApiKey(), ID_0.getId()); + + final Collection results = gameListing.getGameNamesPlayerHasJoined(user); + + assertThat( + "Player was in one game, but it was removed, should be empty", results, is(empty())); + } + + @Test + void postingGameAddsPlayersFromThatGame() { + gameListing.postGame( + API_KEY_0, + GamePostingRequest.builder() + .lobbyGame(lobbyGame0) + .playerNames(List.of("player1", "player2")) + .build()); + + Collection results = gameListing.getGameNamesPlayerHasJoined(UserName.of("player1")); + + assertThat(results, hasSize(1)); + assertThat(results, hasItem(lobbyGame0.getHostName())); + + // verify player2 was added + results = gameListing.getGameNamesPlayerHasJoined(UserName.of("player2")); + + assertThat(results, hasSize(1)); + assertThat(results, hasItem(lobbyGame0.getHostName())); + } + } + + @Nested + class PlayersInGame { + @Test + void emptyCase() { + assertThat(gameListing.getPlayersInGame("game-id"), is(empty())); + } + + @Test + void wrongGameCase() { + cache.put(ID_0, lobbyGame0); + + assertThat(gameListing.getPlayersInGame("DNE"), is(empty())); + } + + @Test + void hasPlayers() { + cache.put(ID_0, lobbyGame0); + + gameListing.addPlayerToGame(UserName.of("player1"), ID_0.getApiKey(), ID_0.getId()); + gameListing.addPlayerToGame(UserName.of("player2"), ID_0.getApiKey(), ID_0.getId()); + + assertThat(gameListing.getPlayersInGame(ID_0.getId()), hasSize(2)); + assertThat(gameListing.getPlayersInGame(ID_0.getId()), hasItems("player1", "player2")); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameTtlExpiredListenerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameTtlExpiredListenerTest.java new file mode 100644 index 0000000..3ceb5bb --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/game/listing/GameTtlExpiredListenerTest.java @@ -0,0 +1,28 @@ +package org.triplea.modules.game.listing; + +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.TestData; +import org.triplea.http.client.web.socket.messages.envelopes.game.listing.LobbyGameRemovedMessage; +import org.triplea.web.socket.WebSocketMessagingBus; + +@ExtendWith(MockitoExtension.class) +class GameTtlExpiredListenerTest { + @Mock private WebSocketMessagingBus playerMessagingBus; + + @InjectMocks private GameTtlExpiredListener gameTtlExpiredListener; + + @Test + void verifyGameRemovedCall() { + final GameListing.GameId gameId = new GameListing.GameId(TestData.API_KEY, "id"); + + gameTtlExpiredListener.accept(gameId, TestData.LOBBY_GAME); + + verify(playerMessagingBus).broadcastMessage(new LobbyGameRemovedMessage("id")); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ChatUploadModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ChatUploadModuleTest.java new file mode 100644 index 0000000..8dbb74a --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ChatUploadModuleTest.java @@ -0,0 +1,59 @@ +package org.triplea.modules.game.lobby.watcher; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.function.BiPredicate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.lobby.games.LobbyGameDao; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.ChatMessageUpload; + +@ExtendWith(MockitoExtension.class) +class ChatUploadModuleTest { + private static final ChatMessageUpload CHAT_MESSAGE_UPLOAD = + ChatMessageUpload.builder() + .gameId("game-id") + .chatMessage("message") + .fromPlayer("player") + .build(); + + @Mock private LobbyGameDao lobbyGameDao; + @Mock private BiPredicate gameIdValidator; + + @InjectMocks private ChatUploadModule chatUploadModule; + + @Test + void validApiAndGameIdPair() { + givenApiKeyIsValid(true); + + final boolean result = chatUploadModule.upload(ApiKey.newKey().getValue(), CHAT_MESSAGE_UPLOAD); + + assertThat(result, is(true)); + verify(lobbyGameDao).recordChat(CHAT_MESSAGE_UPLOAD); + } + + @Test + void inValidApiAndGameIdPair() { + givenApiKeyIsValid(false); + + final boolean result = chatUploadModule.upload(ApiKey.newKey().getValue(), CHAT_MESSAGE_UPLOAD); + + assertThat(result, is(false)); + verify(lobbyGameDao, never()).recordChat(any()); + } + + private void givenApiKeyIsValid(final boolean isValid) { + when(gameIdValidator.test(Mockito.any(), Mockito.eq(CHAT_MESSAGE_UPLOAD.getGameId()))) + .thenReturn(isValid); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheckTest.java b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheckTest.java new file mode 100644 index 0000000..c07123a --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/ConnectivityCheckTest.java @@ -0,0 +1,52 @@ +package org.triplea.modules.game.lobby.watcher; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ConnectivityCheckTest { + @Mock private Socket socket; + + private ConnectivityCheck connectivityCheck; + + @BeforeEach + void setUp() { + connectivityCheck = new ConnectivityCheck(() -> socket); + } + + @Test + void connectionSuccess() throws IOException { + when(socket.isConnected()).thenReturn(true); + + final boolean result = connectivityCheck.canDoReverseConnect("game-host-address", 123); + + assertThat(result, is(true)); + verify(socket).close(); + } + + @Test + void connectionFailure() throws IOException { + doThrow(new IOException("simulated exception")) + .when(socket) + .connect(eq(new InetSocketAddress("game-host-address", 123)), anyInt()); + + final boolean result = connectivityCheck.canDoReverseConnect("game-host-address", 123); + + assertThat(result, is(false)); + verify(socket).close(); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/GamePostingModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/GamePostingModuleTest.java new file mode 100644 index 0000000..859b855 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/game/lobby/watcher/GamePostingModuleTest.java @@ -0,0 +1,59 @@ +package org.triplea.modules.game.lobby.watcher; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.TestData; +import org.triplea.domain.data.ApiKey; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingRequest; +import org.triplea.http.client.lobby.game.lobby.watcher.GamePostingResponse; +import org.triplea.modules.game.listing.GameListing; + +@ExtendWith(MockitoExtension.class) +class GamePostingModuleTest { + private static final GamePostingRequest gamePostingRequest = + GamePostingRequest.builder().lobbyGame(TestData.LOBBY_GAME).build(); + + @Mock private GameListing gameListing; + @Mock private ConnectivityCheck connectivityCheck; + + @InjectMocks private GamePostingModule gamePostingModule; + + @Test + void connectivityCheckSucceeds() { + when(connectivityCheck.canDoReverseConnect( + TestData.LOBBY_GAME.getHostAddress(), TestData.LOBBY_GAME.getHostPort())) + .thenReturn(true); + when(gameListing.postGame(ApiKey.of("api-key"), gamePostingRequest)).thenReturn("game-id"); + + final GamePostingResponse gamePostingResponse = + gamePostingModule.postGame(ApiKey.of("api-key"), gamePostingRequest); + + assertThat(gamePostingResponse.isConnectivityCheckSucceeded(), is(true)); + assertThat(gamePostingResponse.getGameId(), is("game-id")); + } + + @Test + void connectivityCheckFails() { + when(connectivityCheck.canDoReverseConnect( + TestData.LOBBY_GAME.getHostAddress(), TestData.LOBBY_GAME.getHostPort())) + .thenReturn(false); + + final GamePostingResponse gamePostingResponse = + gamePostingModule.postGame(ApiKey.of("api-key"), gamePostingRequest); + + assertThat(gamePostingResponse.isConnectivityCheckSucceeded(), is(false)); + assertThat(gamePostingResponse.getGameId(), is(nullValue())); + verify(gameListing, never()).postGame(any(), any()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/access/log/AccessLogServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/access/log/AccessLogServiceTest.java new file mode 100644 index 0000000..5445121 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/access/log/AccessLogServiceTest.java @@ -0,0 +1,71 @@ +package org.triplea.modules.moderation.access.log; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.access.log.AccessLogDao; +import org.triplea.db.dao.access.log.AccessLogRecord; +import org.triplea.http.client.lobby.moderator.toolbox.PagingParams; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogData; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogRequest; +import org.triplea.http.client.lobby.moderator.toolbox.log.AccessLogSearchRequest; + +@ExtendWith(MockitoExtension.class) +class AccessLogServiceTest { + private static final PagingParams PAGING_PARAMS = + PagingParams.builder().pageSize(100).rowNumber(0).build(); + + private static final AccessLogRecord ACCESS_LOG_DAO_DATA = + AccessLogRecord.builder() + .accessTime(Instant.now()) + .username("username") + .ip("ip") + .systemId("system-id") + .registered(true) + .build(); + + @Mock private AccessLogDao accessLogDao; + + @InjectMocks private AccessLogService accessLogService; + + @Test + void fetchAccessLog() { + when(accessLogDao.fetchAccessLogRows( + PAGING_PARAMS.getRowNumber(), + PAGING_PARAMS.getPageSize(), + "username", + "1.2.3.4", + "system-id")) + .thenReturn(List.of(ACCESS_LOG_DAO_DATA)); + + final List results = + accessLogService.fetchAccessLog( + AccessLogRequest.builder() + .accessLogSearchRequest( + AccessLogSearchRequest.builder() + .username("username") + .systemId("system-id") + .ip("1.2.3.4") + .build()) + .pagingParams(PAGING_PARAMS) + .build()); + + assertThat(results, hasSize(1)); + + assertThat( + results.get(0).getAccessDate(), is(ACCESS_LOG_DAO_DATA.getAccessTime().toEpochMilli())); + assertThat(results.get(0).getSystemId(), is(ACCESS_LOG_DAO_DATA.getSystemId())); + assertThat(results.get(0).getIp(), is(ACCESS_LOG_DAO_DATA.getIp())); + assertThat(results.get(0).getUsername(), is(ACCESS_LOG_DAO_DATA.getUsername())); + assertThat(results.get(0).isRegistered(), is(ACCESS_LOG_DAO_DATA.isRegistered())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryServiceTest.java new file mode 100644 index 0000000..dddaa97 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/audit/history/ModeratorAuditHistoryServiceTest.java @@ -0,0 +1,65 @@ +package org.triplea.modules.moderation.audit.history; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryRecord; +import org.triplea.http.client.lobby.moderator.toolbox.log.ModeratorEvent; + +@ExtendWith(MockitoExtension.class) +class ModeratorAuditHistoryServiceTest { + + private static final int ROW_OFFSET = 30; + private static final int ROW_COUNT = 70; + + private static final ModeratorAuditHistoryRecord ITEM_1 = + ModeratorAuditHistoryRecord.builder() + .actionName("Jolly courages lead to love.") + .actionTarget("All peglegs hoist evil, small rums.") + .dateCreated(Instant.now()) + .username("Ahoy, yer not lootting me without a yellow fever!") + .build(); + + private static final ModeratorAuditHistoryRecord ITEM_2 = + ModeratorAuditHistoryRecord.builder() + .actionName("Ahoy, endure me kraken, ye old wind!") + .actionTarget("All gulls love salty, swashbuckling pins.") + .dateCreated(Instant.now().minusSeconds(5000)) + .username("Waves travel with urchin at the sunny singapore!") + .build(); + + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @InjectMocks private ModeratorAuditHistoryService moderatorAuditHistoryService; + + @Test + void lookupHistory() { + when(moderatorAuditHistoryDao.lookupHistoryItems(ROW_OFFSET, ROW_COUNT)) + .thenReturn(ImmutableList.of(ITEM_1, ITEM_2)); + + final List results = + moderatorAuditHistoryService.lookupHistory(ROW_OFFSET, ROW_COUNT); + + assertThat(results, hasSize(2)); + assertThat(results.get(0).getDate(), is(ITEM_1.getDateCreated().toEpochMilli())); + assertThat(results.get(0).getActionTarget(), is(ITEM_1.getActionTarget())); + assertThat(results.get(0).getModeratorAction(), is(ITEM_1.getActionName())); + assertThat(results.get(0).getModeratorName(), is(ITEM_1.getUsername())); + + assertThat(results.get(1).getDate(), is(ITEM_2.getDateCreated().toEpochMilli())); + assertThat(results.get(1).getActionTarget(), is(ITEM_2.getActionTarget())); + assertThat(results.get(1).getModeratorAction(), is(ITEM_2.getActionName())); + assertThat(results.get(1).getModeratorName(), is(ITEM_2.getUsername())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/bad/words/BadWordsServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/bad/words/BadWordsServiceTest.java new file mode 100644 index 0000000..f62b82e --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/bad/words/BadWordsServiceTest.java @@ -0,0 +1,77 @@ +package org.triplea.modules.moderation.bad.words; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.BadWordsDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; + +@ExtendWith(MockitoExtension.class) +class BadWordsServiceTest { + + private static final String TEST_VALUE = "some-value"; + private static final ImmutableList BAD_WORDS_SAMPLE = + ImmutableList.of("one", "two", "three"); + + private static final int MODERATOR_ID = 100; + + @Mock private BadWordsDao badWordsDao; + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @InjectMocks private BadWordsService badWordsService; + + @Test + void removeBadWordSuccessCase() { + when(badWordsDao.removeBadWord(TEST_VALUE)).thenReturn(1); + + badWordsService.removeBadWord(MODERATOR_ID, TEST_VALUE); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_BAD_WORD) + .actionTarget(TEST_VALUE) + .build()); + } + + @Test + void addBadWordSuccessCase() { + when(badWordsDao.addBadWord(TEST_VALUE)).thenReturn(1); + + assertThat(badWordsService.addBadWord(MODERATOR_ID, TEST_VALUE), is(true)); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_BAD_WORD) + .actionTarget(TEST_VALUE) + .build()); + } + + @Test + void addBadWordFailureCase() { + when(badWordsDao.addBadWord(TEST_VALUE)).thenReturn(0); + + assertThat(badWordsService.addBadWord(MODERATOR_ID, TEST_VALUE), is(false)); + verify(moderatorAuditHistoryDao, never()).addAuditRecord(any()); + } + + @Test + void getBadWords() { + when(badWordsDao.getBadWords()).thenReturn(BAD_WORDS_SAMPLE); + + assertThat(badWordsService.getBadWords(), is(BAD_WORDS_SAMPLE)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/name/UsernameBanServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/name/UsernameBanServiceTest.java new file mode 100644 index 0000000..0ab1cb4 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/name/UsernameBanServiceTest.java @@ -0,0 +1,105 @@ +package org.triplea.modules.moderation.ban.name; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.username.ban.UsernameBanDao; +import org.triplea.db.dao.username.ban.UsernameBanRecord; +import org.triplea.http.client.lobby.moderator.toolbox.banned.name.UsernameBanData; + +@ExtendWith(MockitoExtension.class) +class UsernameBanServiceTest { + + private static final String USERNAME = "You haul like an ale."; + private static final int MODERATOR_ID = 42352; + private static final UsernameBanRecord USERNAME_BAN_RECORD = + UsernameBanRecord.builder() + .dateCreated(Instant.now()) + .username("Golden, big mainlands quietly trade a stormy, warm skull.") + .build(); + + @Mock private UsernameBanDao bannedUsernamesDao; + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @InjectMocks private UsernameBanService usernameBanService; + + @Nested + final class RemoveNameBanTest { + + @Test + void removeFailureCase() { + when(bannedUsernamesDao.removeBannedUserName(USERNAME)).thenReturn(0); + + assertThat(usernameBanService.removeUsernameBan(MODERATOR_ID, USERNAME), is(false)); + + verify(moderatorAuditHistoryDao, never()).addAuditRecord(any()); + } + + @Test + void removeSuccessCase() { + when(bannedUsernamesDao.removeBannedUserName(USERNAME)).thenReturn(1); + + assertThat(usernameBanService.removeUsernameBan(MODERATOR_ID, USERNAME), is(true)); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_USERNAME_BAN) + .actionTarget(USERNAME) + .build()); + } + } + + @Nested + final class AddNameBanTest { + @Test + void addFailureCase() { + when(bannedUsernamesDao.addBannedUserName(USERNAME)).thenReturn(0); + + assertThat(usernameBanService.addBannedUserName(MODERATOR_ID, USERNAME), is(false)); + + verify(moderatorAuditHistoryDao, never()).addAuditRecord(any()); + } + + @Test + void addSuccessCase() { + when(bannedUsernamesDao.addBannedUserName(USERNAME)).thenReturn(1); + + assertThat(usernameBanService.addBannedUserName(MODERATOR_ID, USERNAME), is(true)); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.BAN_USERNAME) + .actionTarget(USERNAME) + .build()); + } + } + + @Test + void getBannedUserNames() { + when(bannedUsernamesDao.getBannedUserNames()).thenReturn(List.of(USERNAME_BAN_RECORD)); + + final List results = usernameBanService.getBannedUserNames(); + assertThat(results, hasSize(1)); + assertThat( + results.get(0).getBanDate(), is(USERNAME_BAN_RECORD.getDateCreated().toEpochMilli())); + assertThat(results.get(0).getBannedName(), is(USERNAME_BAN_RECORD.getUsername())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandlerTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandlerTest.java new file mode 100644 index 0000000..4d4a99f --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/BannedPlayerEventHandlerTest.java @@ -0,0 +1,29 @@ +package org.triplea.modules.moderation.ban.user; + +import static org.mockito.Mockito.verify; + +import java.net.InetAddress; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.java.IpAddressParser; +import org.triplea.web.socket.SessionSet; + +@ExtendWith(MockitoExtension.class) +class BannedPlayerEventHandlerTest { + + private static final InetAddress IP = IpAddressParser.fromString("99.88.77.66"); + @Mock private SessionSet sessionSet; + + @Test + void fireBannedEvent() { + final BannedPlayerEventHandler bannedPlayerEventHandler = + BannedPlayerEventHandler.builder().sessionSets(Set.of(sessionSet)).build(); + + bannedPlayerEventHandler.fireBannedEvent(IP); + + verify(sessionSet).closeSessionsByIp(IP); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/UserBanServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/UserBanServiceTest.java new file mode 100644 index 0000000..760b9ed --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/ban/user/UserBanServiceTest.java @@ -0,0 +1,166 @@ +package org.triplea.modules.moderation.ban.user; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import org.hamcrest.collection.IsCollectionWithSize; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.db.dao.user.ban.UserBanRecord; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanData; +import org.triplea.http.client.lobby.moderator.toolbox.banned.user.UserBanParams; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; + +@ExtendWith(MockitoExtension.class) +class UserBanServiceTest { + + private static final int MODERATOR_ID = 123; + private static final String BAN_ID = "Parrots grow with pestilence at the sunny madagascar!"; + private static final String USERNAME = "Aye, never taste a bilge rat."; + + private static final UserBanRecord USER_BAN_RECORD_1 = + UserBanRecord.builder() + .systemId("Suns stutter from madness like addled comrades.") + .publicBanId("Ah, never endure a mast.") + .banExpiry(Instant.now().plus(1, ChronoUnit.DAYS)) + .dateCreated(Instant.now()) + .ip("33.99.99.99") + .username("How old. You drink like a jolly roger.") + .build(); + private static final UserBanRecord USER_BAN_RECORD_2 = + UserBanRecord.builder() + .systemId("") + .publicBanId("") + .banExpiry(Instant.now().plus(2, ChronoUnit.DAYS)) + .dateCreated(Instant.now().minus(2, ChronoUnit.HOURS)) + .ip("55.99.99.99") + .username("The buccaneer desires with love, break the bahamas until it travels.") + .build(); + + private static final UserBanParams USER_BAN_PARAMS = + UserBanParams.builder() + .username(USERNAME) + .systemId("Love ho! fight to be desired.") + .ip("99.99.00.99") + .minutesToBan(20) + .build(); + + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + @Mock private UserBanDao userBanDao; + @Mock private Supplier publicIdSupplier; + @Mock private Chatters chatters; + + @SuppressWarnings("unused") // injected into UserBanService + @Mock + private PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + + @Mock private WebSocketMessagingBus chatMessagingBus; + @Mock private WebSocketMessagingBus gameMessagingBus; + + private UserBanService bannedUsersService; + + @BeforeEach + void setUp() { + bannedUsersService = + new UserBanService( + moderatorAuditHistoryDao, + userBanDao, + publicIdSupplier, + chatters, + apiKeyDaoWrapper, + chatMessagingBus, + gameMessagingBus); + } + + @Test + void getBannedUsers() { + when(userBanDao.lookupBans()).thenReturn(List.of(USER_BAN_RECORD_1, USER_BAN_RECORD_2)); + + final List result = bannedUsersService.getBannedUsers(); + + assertThat(result, IsCollectionWithSize.hasSize(2)); + + assertThat(result.get(0).getBanDate(), is(USER_BAN_RECORD_1.getDateCreated().toEpochMilli())); + assertThat(result.get(0).getBanExpiry(), is(USER_BAN_RECORD_1.getBanExpiry().toEpochMilli())); + assertThat(result.get(0).getBanId(), is(USER_BAN_RECORD_1.getPublicBanId())); + assertThat(result.get(0).getHashedMac(), is(USER_BAN_RECORD_1.getSystemId())); + assertThat(result.get(0).getIp(), is(USER_BAN_RECORD_1.getIp())); + assertThat(result.get(0).getUsername(), is(USER_BAN_RECORD_1.getUsername())); + + assertThat(result.get(1).getBanDate(), is(USER_BAN_RECORD_2.getDateCreated().toEpochMilli())); + assertThat(result.get(1).getBanExpiry(), is(USER_BAN_RECORD_2.getBanExpiry().toEpochMilli())); + assertThat(result.get(1).getBanId(), is(USER_BAN_RECORD_2.getPublicBanId())); + assertThat(result.get(1).getHashedMac(), is(USER_BAN_RECORD_2.getSystemId())); + assertThat(result.get(1).getIp(), is(USER_BAN_RECORD_2.getIp())); + assertThat(result.get(1).getUsername(), is(USER_BAN_RECORD_2.getUsername())); + } + + @Nested + final class RemoveUserBanTest { + + @Test + void removeUserBanFailureCase() { + when(userBanDao.removeBan(BAN_ID)).thenReturn(0); + + final boolean result = bannedUsersService.removeUserBan(MODERATOR_ID, BAN_ID); + + assertThat(result, is(false)); + verify(moderatorAuditHistoryDao, never()).addAuditRecord(any()); + } + + @Test + void removeUserBanSuccessCase() { + when(userBanDao.removeBan(BAN_ID)).thenReturn(1); + when(userBanDao.lookupUsernameByBanId(BAN_ID)).thenReturn(Optional.of(USERNAME)); + + final boolean result = bannedUsersService.removeUserBan(MODERATOR_ID, BAN_ID); + + assertThat(result, is(true)); + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_USER_BAN) + .actionTarget(USERNAME) + .moderatorUserId(MODERATOR_ID) + .build()); + } + } + + @Test + void banUserFailureCase() { + givenBanDaoUpdateCount(0); + + assertThrows( + IllegalStateException.class, + () -> bannedUsersService.banUser(MODERATOR_ID, USER_BAN_PARAMS)); + } + + private void givenBanDaoUpdateCount(final int updateCount) { + when(publicIdSupplier.get()).thenReturn(BAN_ID); + when(userBanDao.addBan( + BAN_ID, + USER_BAN_PARAMS.getUsername(), + USER_BAN_PARAMS.getSystemId(), + USER_BAN_PARAMS.getIp(), + USER_BAN_PARAMS.getMinutesToBan())) + .thenReturn(updateCount); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModuleTest.java new file mode 100644 index 0000000..c731b8a --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/chat/history/FetchGameChatHistoryModuleTest.java @@ -0,0 +1,60 @@ +package org.triplea.modules.moderation.chat.history; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.chat.history.ChatHistoryRecord; +import org.triplea.db.dao.moderator.chat.history.GameChatHistoryDao; +import org.triplea.http.client.lobby.moderator.ChatHistoryMessage; + +@ExtendWith(MockitoExtension.class) +class FetchGameChatHistoryModuleTest { + + private static final ChatHistoryRecord CHAT_HISTORY_RECORD_0 = + ChatHistoryRecord.builder() // + .username("user") + .message("message") + .date(Instant.now()) + .build(); + + private static final ChatHistoryRecord CHAT_HISTORY_RECORD_1 = + ChatHistoryRecord.builder() // + .username("user2") + .message(" ") + .date(Instant.EPOCH) + .build(); + + @Mock private GameChatHistoryDao gameChatHistoryDao; + + @InjectMocks private FetchGameChatHistoryModule fetchGameChatHistoryModule; + + @Test + void emptyCase() { + when(gameChatHistoryDao.getChatHistory("game-id")).thenReturn(List.of()); + + assertThat(fetchGameChatHistoryModule.apply("game-id"), is(empty())); + } + + @Test + void convertsChatRecordsToChatHistoryMessagesInOrder() { + when(gameChatHistoryDao.getChatHistory("game-id")) + .thenReturn(List.of(CHAT_HISTORY_RECORD_0, CHAT_HISTORY_RECORD_1)); + + final List results = fetchGameChatHistoryModule.apply("game-id"); + + assertThat(results, hasSize(2)); + + assertThat(results.get(0), is(CHAT_HISTORY_RECORD_0.toChatHistoryMessage())); + assertThat(results.get(1), is(CHAT_HISTORY_RECORD_1.toChatHistoryMessage())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserActionTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserActionTest.java new file mode 100644 index 0000000..6418c67 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/disconnect/user/DisconnectUserActionTest.java @@ -0,0 +1,135 @@ +package org.triplea.modules.moderation.disconnect.user; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.web.socket.MessageEnvelope; +import org.triplea.http.client.web.socket.messages.envelopes.chat.ChatEventReceivedMessage; +import org.triplea.modules.chat.Chatters; +import org.triplea.web.socket.WebSocketMessagingBus; + +@SuppressWarnings("InnerClassMayBeStatic") +@ExtendWith(MockitoExtension.class) +class DisconnectUserActionTest { + + private static final int MODERATOR_ID = 100; + private static final PlayerChatId PLAYER_CHAT_ID = PlayerChatId.of("player-chat-id"); + private static final PlayerIdentifiersByApiKeyLookup PLAYER_ID_LOOKUP = + PlayerIdentifiersByApiKeyLookup.builder() + .ip("99.99.99.99") + .userName("player-name") + .systemId("system-id") + .build(); + + @Mock private PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + @Mock private Chatters chatters; + @Mock private WebSocketMessagingBus playerConnections; + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @InjectMocks private DisconnectUserAction disconnectUserAction; + + @Nested + class DisconnectPlayer { + @Test + @DisplayName("Verify disconnect return false if player API key is not found") + void noOpIfPlayerApiKeyNotPresent() { + when(apiKeyDaoWrapper.lookupPlayerByChatId(PLAYER_CHAT_ID)).thenReturn(Optional.empty()); + + assertThat(disconnectUserAction.disconnectPlayer(MODERATOR_ID, PLAYER_CHAT_ID), is(false)); + + verifyNoOp(); + } + + private void verifyNoOp() { + verify(chatters, never()).disconnectPlayerByName(any(), any()); + verify(playerConnections, never()).broadcastMessage(any(MessageEnvelope.class)); + verify(moderatorAuditHistoryDao, never()).addAuditRecord(any()); + } + + @Test + @DisplayName("Verify disconnect if player is not found") + void noOpIfPlayerNotPresentInChat() { + givenApiKeyLookupButPlayerNotInChat(); + + assertThat(disconnectUserAction.disconnectPlayer(MODERATOR_ID, PLAYER_CHAT_ID), is(false)); + + verifyNoOp(); + } + + private void givenApiKeyLookupButPlayerNotInChat() { + when(apiKeyDaoWrapper.lookupPlayerByChatId(PLAYER_CHAT_ID)) + .thenReturn(Optional.of(PLAYER_ID_LOOKUP)); + when(chatters.isPlayerConnected(PLAYER_ID_LOOKUP.getUserName())).thenReturn(false); + } + + @Test + @DisplayName( + "When player disconnected, verify audit recorded, chatters notified, " + + "and player is disconnected") + void playerDisconnect() { + when(apiKeyDaoWrapper.lookupPlayerByChatId(PLAYER_CHAT_ID)) + .thenReturn(Optional.of(PLAYER_ID_LOOKUP)); + when(chatters.isPlayerConnected(PLAYER_ID_LOOKUP.getUserName())).thenReturn(true); + when(chatters.disconnectPlayerByName(eq(PLAYER_ID_LOOKUP.getUserName()), any())) + .thenReturn(true); + + disconnectUserAction.disconnectPlayer(MODERATOR_ID, PLAYER_CHAT_ID); + + verifyPlayerIsDisconnected(); + verifyChattersAreNotified(); + verifyDisconnectIsRecorded(); + } + + private void verifyPlayerIsDisconnected() { + final ArgumentCaptor disconnectMessageCaptor = ArgumentCaptor.forClass(String.class); + verify(chatters) + .disconnectPlayerByName( + eq(PLAYER_ID_LOOKUP.getUserName()), disconnectMessageCaptor.capture()); + assertThat( + "Disconnect message should contain the word 'disconnect'", + disconnectMessageCaptor.getValue().toLowerCase(), + containsString("disconnect")); + } + + private void verifyChattersAreNotified() { + final ArgumentCaptor eventMessageCaptor = + ArgumentCaptor.forClass(ChatEventReceivedMessage.class); + verify(playerConnections).broadcastMessage(eventMessageCaptor.capture()); + assertThat( + "Message type is chat event", + eventMessageCaptor.getValue().toEnvelope().getMessageTypeId(), + is(ChatEventReceivedMessage.TYPE.getMessageTypeId())); + assertThat( + "Disconnect message to chatters contains 'was disconnected'", + eventMessageCaptor.getValue().getMessage(), + containsString("was disconnected")); + assertThat( + "Disconnect message contains player name", + eventMessageCaptor.getValue().getMessage(), + containsString(PLAYER_ID_LOOKUP.getUserName().getValue())); + } + + private void verifyDisconnectIsRecorded() { + verify(moderatorAuditHistoryDao).addAuditRecord(any()); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/moderators/ModeratorsServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/moderators/ModeratorsServiceTest.java new file mode 100644 index 0000000..9db4b23 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/moderators/ModeratorsServiceTest.java @@ -0,0 +1,156 @@ +package org.triplea.modules.moderation.moderators; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.moderator.ModeratorsDao; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; + +@ExtendWith(MockitoExtension.class) +class ModeratorsServiceTest { + private static final String MODERATOR_NAME = "Adventure is a heavy-hearted sailor."; + private static final int USER_ID = 1234; + private static final int MODERATOR_ID = 555; + private static final String USERNAME = "The reef grows amnesty like a golden lass."; + + @Mock private ModeratorsDao moderatorsDao; + @Mock private UserJdbiDao userJdbiDao; + @Mock private ModeratorAuditHistoryDao moderatorAuditHistoryDao; + + @InjectMocks private ModeratorsService moderatorsService; + + @Nested + final class AddModeratorTest { + @Test + void throwsIfUserNotFound() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.empty()); + assertThrows( + IllegalArgumentException.class, + () -> moderatorsService.addModerator(MODERATOR_ID, USERNAME)); + } + + @Test + void throwsIfModeratorNotAdded() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.MODERATOR)).thenReturn(0); + assertThrows( + IllegalStateException.class, + () -> moderatorsService.addModerator(MODERATOR_ID, USERNAME)); + } + + @Test + void verifySuccessCase() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.MODERATOR)).thenReturn(1); + + moderatorsService.addModerator(MODERATOR_ID, USERNAME); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_MODERATOR) + .actionTarget(USERNAME) + .build()); + } + } + + @Nested + final class RemoveModTest { + @Test + void throwsIfModNameIsNotFound() { + when(userJdbiDao.lookupUserIdByName(MODERATOR_NAME)).thenReturn(Optional.empty()); + assertThrows( + IllegalArgumentException.class, + () -> moderatorsService.removeMod(MODERATOR_ID, MODERATOR_NAME)); + } + + @Test + void throwsIfModIsNotRemoved() { + when(userJdbiDao.lookupUserIdByName(MODERATOR_NAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.PLAYER)).thenReturn(0); + + assertThrows( + IllegalStateException.class, + () -> moderatorsService.removeMod(MODERATOR_ID, MODERATOR_NAME)); + } + + @Test + void verifySuccessfulRemove() { + when(userJdbiDao.lookupUserIdByName(MODERATOR_NAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.PLAYER)).thenReturn(1); + + moderatorsService.removeMod(MODERATOR_ID, MODERATOR_NAME); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.REMOVE_MODERATOR) + .actionTarget(MODERATOR_NAME) + .build()); + } + } + + @Nested + final class AddAdminTest { + @Test + void throwsIfUserNotFound() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.empty()); + assertThrows( + IllegalArgumentException.class, () -> moderatorsService.addAdmin(MODERATOR_ID, USERNAME)); + } + + @Test + void throwsIfAdminNotAdded() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.ADMIN)).thenReturn(0); + + assertThrows( + IllegalStateException.class, () -> moderatorsService.addAdmin(MODERATOR_ID, USERNAME)); + } + + @Test + void verifySuccessfulAdd() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.of(USER_ID)); + when(moderatorsDao.setRole(USER_ID, UserRole.ADMIN)).thenReturn(1); + + moderatorsService.addAdmin(MODERATOR_ID, USERNAME); + + verify(moderatorAuditHistoryDao) + .addAuditRecord( + ModeratorAuditHistoryDao.AuditArgs.builder() + .moderatorUserId(MODERATOR_ID) + .actionName(ModeratorAuditHistoryDao.AuditAction.ADD_SUPER_MOD) + .actionTarget(USERNAME) + .build()); + } + } + + @Nested + final class UserExistsTest { + @Test + void userDoesNotExist() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.empty()); + assertThat(moderatorsService.userExistsByName(USERNAME), is(false)); + } + + @Test + void userExists() { + when(userJdbiDao.lookupUserIdByName(USERNAME)).thenReturn(Optional.of(USER_ID)); + assertThat(moderatorsService.userExistsByName(USERNAME), is(true)); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModuleTest.java new file mode 100644 index 0000000..431e239 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/moderation/remote/actions/RemoteActionsModuleTest.java @@ -0,0 +1,86 @@ +package org.triplea.modules.moderation.remote.actions; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao.AuditAction; +import org.triplea.db.dao.moderator.ModeratorAuditHistoryDao.AuditArgs; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.http.client.web.socket.messages.envelopes.remote.actions.ShutdownServerMessage; +import org.triplea.java.IpAddressParser; +import org.triplea.web.socket.WebSocketMessagingBus; + +@ExtendWith(MockitoExtension.class) +class RemoteActionsModuleTest { + + private static final String IP = "99.55.33.11"; + private static final int MODERATOR_ID = 300; + + @Mock private UserBanDao userBanDao; + @Mock private ModeratorAuditHistoryDao auditHistoryDao; + @Mock private WebSocketMessagingBus gameMessagingBus; + + private RemoteActionsModule remoteActionsModule; + + @BeforeEach + void setUp() { + remoteActionsModule = + RemoteActionsModule.builder() + .userBanDao(userBanDao) + .auditHistoryDao(auditHistoryDao) + .gameMessagingBus(gameMessagingBus) + .build(); + } + + @Nested + class IsUserBanned { + @Test + void userIsBanned() { + when(userBanDao.isBannedByIp(IP)).thenReturn(true); + + final boolean result = remoteActionsModule.isUserBanned(IpAddressParser.fromString(IP)); + + assertThat(result, is(true)); + } + + @Test + void userIsNotBanned() { + when(userBanDao.isBannedByIp(IP)).thenReturn(false); + + final boolean result = remoteActionsModule.isUserBanned(IpAddressParser.fromString(IP)); + + assertThat(result, is(false)); + } + } + + @Nested + class AddGameIdforShutdown { + @Test + void addGameIdforShutdown() { + remoteActionsModule.addGameIdForShutdown(MODERATOR_ID, "game-id"); + + verify(gameMessagingBus).broadcastMessage(new ShutdownServerMessage("game-id")); + + final ArgumentCaptor capture = ArgumentCaptor.forClass(AuditArgs.class); + verify(auditHistoryDao).addAuditRecord(capture.capture()); + assertThat( + capture.getValue(), + is( + AuditArgs.builder() + .actionName(AuditAction.REMOTE_SHUTDOWN) + .actionTarget("game-id") + .moderatorUserId(MODERATOR_ID) + .build())); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/player/info/FetchPlayerInfoModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/player/info/FetchPlayerInfoModuleTest.java new file mode 100644 index 0000000..8607067 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/player/info/FetchPlayerInfoModuleTest.java @@ -0,0 +1,209 @@ +package org.triplea.modules.player.info; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.api.key.PlayerApiKeyDaoWrapper; +import org.triplea.db.dao.api.key.PlayerIdentifiersByApiKeyLookup; +import org.triplea.db.dao.moderator.player.info.PlayerAliasRecord; +import org.triplea.db.dao.moderator.player.info.PlayerBanRecord; +import org.triplea.db.dao.moderator.player.info.PlayerInfoForModeratorDao; +import org.triplea.db.dao.user.history.PlayerHistoryDao; +import org.triplea.db.dao.user.history.PlayerHistoryRecord; +import org.triplea.domain.data.ChatParticipant; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.http.client.lobby.moderator.PlayerSummary.Alias; +import org.triplea.http.client.lobby.moderator.PlayerSummary.BanInformation; +import org.triplea.java.IpAddressParser; +import org.triplea.modules.chat.ChatterSession; +import org.triplea.modules.chat.Chatters; +import org.triplea.modules.game.listing.GameListing; +import org.triplea.web.socket.WebSocketSession; + +@ExtendWith(MockitoExtension.class) +class FetchPlayerInfoModuleTest { + + private static final PlayerIdentifiersByApiKeyLookup GAME_PLAYER_LOOKUP = + PlayerIdentifiersByApiKeyLookup.builder() + .ip("1.1.1.1") + .systemId("system-id") + .userName("user-name") + .build(); + + private static final PlayerAliasRecord PLAYER_ALIAS_RECORD = + PlayerAliasRecord.builder() + .username("alias-user-name") + .systemId("system-id2") + .ip("2.3.2.3") + .accessTime(LocalDateTime.of(2000, 1, 1, 1, 1, 1).toInstant(ZoneOffset.UTC)) + .build(); + + private static final PlayerBanRecord PLAYER_BAN_RECORD = + PlayerBanRecord.builder() + .username("banned-name") + .systemId("id-at-time-of-ban") + .ip("5.5.5.6") + .banStart(LocalDateTime.of(2001, 1, 1, 1, 1, 1).toInstant(ZoneOffset.UTC)) + .banEnd(LocalDateTime.of(2100, 1, 1, 1, 1, 1).toInstant(ZoneOffset.UTC)) + .build(); + + @Mock private PlayerApiKeyDaoWrapper apiKeyDaoWrapper; + @Mock private PlayerInfoForModeratorDao playerInfoForModeratorDao; + @Mock private PlayerHistoryDao playerHistoryDao; + @Mock private Chatters chatters; + @Mock private GameListing gameListing; + + @InjectMocks private FetchPlayerInfoModule fetchPlayerInfoModule; + + private ChatterSession chatterSession; + + @BeforeEach + void setupChatterSessionData() { + chatterSession = + ChatterSession.builder() + .ip(IpAddressParser.fromString("1.2.3.4")) + .apiKeyId(-1) + .chatParticipant( + ChatParticipant.builder() + .userName("user-name") + .isModerator(false) + .status("AFK") + .playerChatId("player-chat-id") + .build()) + .session(mock(WebSocketSession.class)) + .build(); + } + + @Test + void unableToFindPlayerChatIdInChattersThrows() { + when(chatters.lookupPlayerByChatId(PlayerChatId.of("id"))).thenReturn(Optional.empty()); + + assertThrows( + IllegalArgumentException.class, + () -> fetchPlayerInfoModule.fetchPlayerInfo(PlayerChatId.of("id"))); + } + + @Test + void unableToFindPlayerChatIdInApiKeyTableThrows() { + when(chatters.lookupPlayerByChatId(PlayerChatId.of("id"))) + .thenReturn(Optional.of(chatterSession)); + when(apiKeyDaoWrapper.lookupPlayerByChatId(PlayerChatId.of("id"))) // + .thenReturn(Optional.empty()); + assertThrows( + IllegalArgumentException.class, + () -> fetchPlayerInfoModule.fetchPlayerInfoAsModerator(PlayerChatId.of("id")), + "Using the lookup play info function requires a player to be in the lobby, " + + "we therefore expect to find their play id, or else throws."); + } + + @Test + @DisplayName("Verify data transformation retrieving info available to any player") + void playerLookupByPlayer() { + when(chatters.lookupPlayerByChatId(PlayerChatId.of("id"))) + .thenReturn(Optional.of(chatterSession)); + + when(gameListing.getGameNamesPlayerHasJoined(chatterSession.getChatParticipant().getUserName())) + .thenReturn(Set.of("Host1", "Host2")); + + final var playerSummaryForPlayer = fetchPlayerInfoModule.fetchPlayerInfo(PlayerChatId.of("id")); + + assertThat(playerSummaryForPlayer.getCurrentGames(), hasItems("Host1", "Host2")); + + // lookup of more information is reserved to moderator players. + verify(apiKeyDaoWrapper, never()).lookupPlayerByChatId(any()); + } + + @Test + void lookupRegistrationDate() { + givenChatIdToUserIdLookup(PlayerChatId.of("chat-id"), 123); + when(playerHistoryDao.lookupPlayerHistoryByUserId(123)) + .thenReturn(Optional.of(new PlayerHistoryRecord(Instant.ofEpochMilli(5000)))); + + final var playerSummaryForPlayer = + fetchPlayerInfoModule.fetchPlayerInfo(PlayerChatId.of("chat-id")); + + assertThat(playerSummaryForPlayer.getRegistrationDateEpochMillis(), is(5000L)); + } + + private void givenChatIdToUserIdLookup(final PlayerChatId playerChatId, final Integer userId) { + when(chatters.lookupPlayerByChatId(playerChatId)).thenReturn(Optional.of(chatterSession)); + when(apiKeyDaoWrapper.lookupUserIdByChatId(PlayerChatId.of("chat-id"))) + .thenReturn(Optional.ofNullable(userId)); + } + + @Test + void lookupRegistrationDateNotRegisteredUserCase() { + givenChatIdToUserIdLookup(PlayerChatId.of("chat-id"), null); + + final var playerSummaryForPlayer = + fetchPlayerInfoModule.fetchPlayerInfo(PlayerChatId.of("chat-id")); + + assertThat(playerSummaryForPlayer.getRegistrationDateEpochMillis(), is(nullValue())); + } + + @Test + @DisplayName("Verify data transformation into a player summary object") + void playerLookupByModerator() { + when(chatters.lookupPlayerByChatId(PlayerChatId.of("id"))) + .thenReturn(Optional.of(chatterSession)); + when(apiKeyDaoWrapper.lookupPlayerByChatId(PlayerChatId.of("id"))) + .thenReturn(Optional.of(GAME_PLAYER_LOOKUP)); + when(playerInfoForModeratorDao.lookupPlayerAliasRecords( + GAME_PLAYER_LOOKUP.getSystemId().getValue(), GAME_PLAYER_LOOKUP.getIp())) + .thenReturn(List.of(PLAYER_ALIAS_RECORD)); + when(playerInfoForModeratorDao.lookupPlayerBanRecords( + GAME_PLAYER_LOOKUP.getSystemId().getValue(), GAME_PLAYER_LOOKUP.getIp())) + .thenReturn(List.of(PLAYER_BAN_RECORD)); + + final var playerSummaryForModerator = + fetchPlayerInfoModule.fetchPlayerInfoAsModerator(PlayerChatId.of("id")); + assertThat(playerSummaryForModerator.getIp(), is(chatterSession.getIp().toString())); + assertThat( + playerSummaryForModerator.getSystemId(), is(GAME_PLAYER_LOOKUP.getSystemId().getValue())); + + assertThat(playerSummaryForModerator.getBans(), hasSize(1)); + assertThat( + playerSummaryForModerator.getBans().iterator().next(), + is( + BanInformation.builder() + .ip(PLAYER_BAN_RECORD.getIp()) + .systemId(PLAYER_BAN_RECORD.getSystemId()) + .name(PLAYER_BAN_RECORD.getUsername()) + .epochMilliStartDate(PLAYER_BAN_RECORD.getBanStart().toEpochMilli()) + .epochMillEndDate(PLAYER_BAN_RECORD.getBanEnd().toEpochMilli()) + .build())); + + assertThat(playerSummaryForModerator.getAliases(), hasSize(1)); + assertThat( + playerSummaryForModerator.getAliases().iterator().next(), + is( + Alias.builder() + .systemId(PLAYER_ALIAS_RECORD.getSystemId()) + .name(PLAYER_ALIAS_RECORD.getUsername()) + .ip(PLAYER_ALIAS_RECORD.getIp()) + .epochMilliDate(PLAYER_ALIAS_RECORD.getDate().toEpochMilli()) + .build())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameIsAvailableValidationTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameIsAvailableValidationTest.java new file mode 100644 index 0000000..b5f0a2a --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameIsAvailableValidationTest.java @@ -0,0 +1,40 @@ +package org.triplea.modules.user.account; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; + +@ExtendWith(MockitoExtension.class) +class NameIsAvailableValidationTest { + + @Mock private UserJdbiDao userJdbiDao; + + @InjectMocks private NameIsAvailableValidation nameIsAvailableValidation; + + @Test + void userAlreadyExists() { + when(userJdbiDao.lookupUserIdByName("name")).thenReturn(Optional.of(1)); + + final Optional result = nameIsAvailableValidation.apply("name"); + + assertThat(result, isPresent()); + } + + @Test + void userDoesNotAlreadyExists() { + when(userJdbiDao.lookupUserIdByName("name")).thenReturn(Optional.empty()); + + final Optional result = nameIsAvailableValidation.apply("name"); + + assertThat(result, isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameValidationTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameValidationTest.java new file mode 100644 index 0000000..c1d4957 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/NameValidationTest.java @@ -0,0 +1,72 @@ +package org.triplea.modules.user.account; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.moderator.BadWordsDao; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.username.ban.UsernameBanDao; + +@ExtendWith(MockitoExtension.class) +class NameValidationTest { + + private static final String NAME = "example-name"; + private static final String ERROR_MESSAGE = "error-sample"; + + @Mock private Function> syntaxValidation; + @Mock private BadWordsDao badWordsDao; + @Mock private UserJdbiDao userJdbiDao; + @Mock private UsernameBanDao usernameBanDao; + + private NameValidation nameValidation; + + @BeforeEach + void setUp() { + nameValidation = + NameValidation.builder() + .syntaxValidation(syntaxValidation) + .badWordsDao(badWordsDao) + .userJdbiDao(userJdbiDao) + .usernameBanDao(usernameBanDao) + .build(); + } + + @Test + void invalidSyntax() { + when(syntaxValidation.apply(NAME)).thenReturn(Optional.of(ERROR_MESSAGE)); + + final Optional result = nameValidation.apply(NAME); + + assertThat(result, isPresentAndIs(ERROR_MESSAGE)); + } + + @Test + void containsBadWord() { + when(syntaxValidation.apply(NAME)).thenReturn(Optional.empty()); + when(badWordsDao.containsBadWord(NAME)).thenReturn(true); + + final Optional result = nameValidation.apply(NAME); + + assertThat(result, isPresent()); + } + + @Test + void valid() { + when(syntaxValidation.apply(NAME)).thenReturn(Optional.empty()); + when(badWordsDao.containsBadWord(NAME)).thenReturn(false); + + final Optional result = nameValidation.apply(NAME); + + assertThat(result, isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/PasswordBCrypterTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/PasswordBCrypterTest.java new file mode 100644 index 0000000..190c166 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/PasswordBCrypterTest.java @@ -0,0 +1,33 @@ +package org.triplea.modules.user.account; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.StringStartsWith.startsWith; + +import org.junit.jupiter.api.Test; + +class PasswordBCrypterTest { + + @Test + void bcrypt() { + final String cryptedResult = PasswordBCrypter.hashPassword("password"); + + assertThat("Simple check to ensure we can invoke the library", cryptedResult, notNullValue()); + + assertThat( + " Bcrypt hashes have a specific starting sequence", cryptedResult, startsWith("$2a$")); + + assertThat(" Bcrypt hashes have a specific length", cryptedResult.length(), is(60)); + } + + @Test + void bcryptHashAndPasswordVerification() { + final String crypted = PasswordBCrypter.hashPassword("password"); + + final boolean result = PasswordBCrypter.verifyHash("password", crypted); + + assertThat( + "Verify BCrypt to match a plaintext password against a crypted password", result, is(true)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/AccountCreatorTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/AccountCreatorTest.java new file mode 100644 index 0000000..100e167 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/AccountCreatorTest.java @@ -0,0 +1,41 @@ +package org.triplea.modules.user.account.create; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.http.client.lobby.login.CreateAccountResponse; + +@ExtendWith(MockitoExtension.class) +class AccountCreatorTest { + private static final String CRYPTED_PASSWORD = "crypted-password"; + private static final CreateAccountRequest CREATE_ACCOUNT_REQUEST = + CreateAccountRequest.builder().username("user").email("email").password("password").build(); + + @Mock private UserJdbiDao userJdbiDao; + @Mock private Function passwordEncryptor; + @InjectMocks private AccountCreator accountCreator; + + @Test + void createAccount() { + when(passwordEncryptor.apply(CREATE_ACCOUNT_REQUEST.getPassword())) + .thenReturn(CRYPTED_PASSWORD); + when(userJdbiDao.createUser( + CREATE_ACCOUNT_REQUEST.getUsername(), + CREATE_ACCOUNT_REQUEST.getEmail(), + CRYPTED_PASSWORD)) + .thenReturn(1); + + final CreateAccountResponse result = accountCreator.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, is(CreateAccountResponse.SUCCESS_RESPONSE)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountModuleTest.java new file mode 100644 index 0000000..8d806fc --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountModuleTest.java @@ -0,0 +1,64 @@ +package org.triplea.modules.user.account.create; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.http.client.lobby.login.CreateAccountRequest; +import org.triplea.http.client.lobby.login.CreateAccountResponse; + +@ExtendWith(MockitoExtension.class) +class CreateAccountModuleTest { + private static final CreateAccountRequest CREATE_ACCOUNT_REQUEST = + CreateAccountRequest.builder().username("username").email("email").password("pass").build(); + private static final String ERROR_MESSAGE = "example error message"; + + @Mock private Function> createAccountValidation; + @Mock private Function accountCreator; + + private CreateAccountModule createAccountModule; + + @BeforeEach + void setUp() { + createAccountModule = + CreateAccountModule.builder() + .createAccountValidation(createAccountValidation) + .accountCreator(accountCreator) + .build(); + } + + @Test + void invalidRequest() { + when(createAccountValidation.apply(CREATE_ACCOUNT_REQUEST)) + .thenReturn(Optional.of(ERROR_MESSAGE)); + + final CreateAccountResponse result = createAccountModule.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result.getErrorMessage(), notNullValue()); + assertThat(result.getErrorMessage(), not(is(CreateAccountResponse.SUCCESS_RESPONSE))); + verify(accountCreator, never()).apply(any()); + } + + @Test + void validRequest() { + when(createAccountValidation.apply(CREATE_ACCOUNT_REQUEST)).thenReturn(Optional.empty()); + when(accountCreator.apply(CREATE_ACCOUNT_REQUEST)) + .thenReturn(CreateAccountResponse.SUCCESS_RESPONSE); + + final CreateAccountResponse result = createAccountModule.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, is(CreateAccountResponse.SUCCESS_RESPONSE)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountValidationTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountValidationTest.java new file mode 100644 index 0000000..9e8134d --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/CreateAccountValidationTest.java @@ -0,0 +1,103 @@ +package org.triplea.modules.user.account.create; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAndIs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.http.client.lobby.login.CreateAccountRequest; + +@ExtendWith(MockitoExtension.class) +class CreateAccountValidationTest { + private static final String ERROR_MESSAGE = "error-message"; + private static final CreateAccountRequest CREATE_ACCOUNT_REQUEST = + CreateAccountRequest.builder() + .username("user") + .email("email@email.com") + .password("password") + .build(); + + @Mock private Function> nameIsAvailableValidator; + @Mock private Function> nameValidator; + @Mock private Function> emailValidator; + @Mock private Function> passwordValidator; + + private CreateAccountValidation createAccountValidation; + + @BeforeEach + void setUp() { + createAccountValidation = + new CreateAccountValidation( + nameValidator, nameIsAvailableValidator, emailValidator, passwordValidator); + } + + @Test + void allValid() { + when(nameValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())).thenReturn(Optional.empty()); + when(nameIsAvailableValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())) + .thenReturn(Optional.empty()); + when(emailValidator.apply(CREATE_ACCOUNT_REQUEST.getEmail())).thenReturn(Optional.empty()); + when(passwordValidator.apply(CREATE_ACCOUNT_REQUEST.getPassword())) + .thenReturn(Optional.empty()); + + final Optional result = createAccountValidation.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, isEmpty()); + } + + @Test + void invalidName() { + when(nameValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())) + .thenReturn(Optional.of(ERROR_MESSAGE)); + + final Optional result = createAccountValidation.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, isPresentAndIs(ERROR_MESSAGE)); + } + + @Test + void nameIsTaken() { + when(nameValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())).thenReturn(Optional.empty()); + + when(nameIsAvailableValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())) + .thenReturn(Optional.of(ERROR_MESSAGE)); + + final Optional result = createAccountValidation.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, isPresentAndIs(ERROR_MESSAGE)); + } + + @Test + void invalidEmail() { + when(nameValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())).thenReturn(Optional.empty()); + when(nameIsAvailableValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())) + .thenReturn(Optional.empty()); + when(emailValidator.apply(CREATE_ACCOUNT_REQUEST.getEmail())) + .thenReturn(Optional.of(ERROR_MESSAGE)); + + final Optional result = createAccountValidation.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, isPresentAndIs(ERROR_MESSAGE)); + } + + @Test + void invalidPassword() { + when(nameValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())).thenReturn(Optional.empty()); + when(nameIsAvailableValidator.apply(CREATE_ACCOUNT_REQUEST.getUsername())) + .thenReturn(Optional.empty()); + when(emailValidator.apply(CREATE_ACCOUNT_REQUEST.getEmail())).thenReturn(Optional.empty()); + when(passwordValidator.apply(CREATE_ACCOUNT_REQUEST.getPassword())) + .thenReturn(Optional.of(ERROR_MESSAGE)); + + final Optional result = createAccountValidation.apply(CREATE_ACCOUNT_REQUEST); + + assertThat(result, isPresentAndIs(ERROR_MESSAGE)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/EmailValidationTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/EmailValidationTest.java new file mode 100644 index 0000000..6e4da4f --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/EmailValidationTest.java @@ -0,0 +1,22 @@ +package org.triplea.modules.user.account.create; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +class EmailValidationTest { + + @Test + void valid() { + assertThat(new EmailValidation().apply("email@test.com"), isEmpty()); + } + + @Test + void invalid() { + assertThat(new EmailValidation().apply(null), isPresent()); + assertThat(new EmailValidation().apply(""), isPresent()); + assertThat(new EmailValidation().apply("invalid"), isPresent()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/PasswordValidationTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/PasswordValidationTest.java new file mode 100644 index 0000000..6b028cd --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/create/PasswordValidationTest.java @@ -0,0 +1,26 @@ +package org.triplea.modules.user.account.create; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.common.base.Strings; +import org.junit.jupiter.api.Test; + +class PasswordValidationTest { + + private static final String VALID = Strings.repeat("a", PasswordValidation.MIN_LENGTH); + private static final String INVALID = Strings.repeat("a", PasswordValidation.MIN_LENGTH - 1); + + private final PasswordValidation passwordValidation = new PasswordValidation(); + + @Test + void valid() { + assertThat(passwordValidation.apply(VALID), isEmpty()); + } + + @Test + void invalid() { + assertThat(passwordValidation.apply(INVALID), isPresent()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/AccessLogUpdaterTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/AccessLogUpdaterTest.java new file mode 100644 index 0000000..5b67047 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/AccessLogUpdaterTest.java @@ -0,0 +1,49 @@ +package org.triplea.modules.user.account.login; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.access.log.AccessLogDao; +import org.triplea.domain.data.PlayerChatId; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; + +@ExtendWith(MockitoExtension.class) +class AccessLogUpdaterTest { + + private static final LoginRecord REGISTERED_LOGIN_RECORD = + LoginRecord.builder() + .systemId(SystemId.of("system-id")) + .playerChatId(PlayerChatId.newId()) + .ip("ip") + .userName(UserName.of("player-name")) + .build(); + + @Mock private AccessLogDao accessLogDao; + + private AccessLogUpdater accessLogUpdater; + + @BeforeEach + void setUp() { + accessLogUpdater = AccessLogUpdater.builder().accessLogDao(accessLogDao).build(); + } + + @Test + void insertUserAccessRecord() { + when(accessLogDao.insertUserAccessRecord(any(), any(), any())).thenReturn(1); + + accessLogUpdater.accept(REGISTERED_LOGIN_RECORD); + + verify(accessLogDao) + .insertUserAccessRecord( + REGISTERED_LOGIN_RECORD.getUserName().getValue(), + REGISTERED_LOGIN_RECORD.getIp(), + REGISTERED_LOGIN_RECORD.getSystemId().getValue()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LobbyLoginMessageDaoTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LobbyLoginMessageDaoTest.java new file mode 100644 index 0000000..46d274e --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LobbyLoginMessageDaoTest.java @@ -0,0 +1,26 @@ +package org.triplea.modules.user.account.login; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; + +import com.github.database.rider.junit5.DBUnitExtension; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.triplea.db.LobbyModuleDatabaseTestSupport; +import org.triplea.test.common.RequiresDatabase; + +@RequiredArgsConstructor +@ExtendWith(LobbyModuleDatabaseTestSupport.class) +@ExtendWith(DBUnitExtension.class) +@RequiresDatabase +class LobbyLoginMessageDaoTest { + + private final LobbyLoginMessageDao lobbyLoginMessageDao; + + @Test + void selectLobbyMessage() { + assertThat(lobbyLoginMessageDao.get(), is(notNullValue())); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LoginModuleTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LoginModuleTest.java new file mode 100644 index 0000000..b877b3c --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/LoginModuleTest.java @@ -0,0 +1,254 @@ +package org.triplea.modules.user.account.login; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.db.dao.user.role.UserRole; +import org.triplea.domain.data.ApiKey; +import org.triplea.domain.data.SystemId; +import org.triplea.domain.data.UserName; +import org.triplea.http.client.lobby.login.LobbyLoginResponse; +import org.triplea.http.client.lobby.login.LoginRequest; + +@ExtendWith(MockitoExtension.class) +class LoginModuleTest { + + private static final SystemId SYSTEM_ID = SystemId.of("system-id"); + private static final String IP = "ip"; + + private static final LoginRequest LOGIN_REQUEST = + LoginRequest.builder().name("name").password("password").build(); + + private static final LoginRequest ANONYMOUS_LOGIN_REQUEST = + LoginRequest.builder().name("name").build(); + + private static final ApiKey API_KEY = ApiKey.of("api-key"); + + private static final String LOBBY_MESSAGE = "Example lobby message"; + + @Mock private Predicate registeredLogin; + @Mock private Predicate tempPasswordLogin; + @Mock private Function> anonymousLogin; + @Mock private Function apiKeyGenerator; + @Mock private Consumer accessLogUpdater; + @Mock private UserJdbiDao userJdbiDao; + @Mock private Function> nameValidation; + + private LoginModule loginModule; + + @BeforeEach + void setUp() { + loginModule = + LoginModule.builder() + .registeredLogin(registeredLogin) + .tempPasswordLogin(tempPasswordLogin) + .anonymousLogin(anonymousLogin) + .apiKeyGenerator(apiKeyGenerator) + .accessLogUpdater(accessLogUpdater) + .userJdbiDao(userJdbiDao) + .nameValidation(nameValidation) + .lobbyLoginMessageDao(() -> LOBBY_MESSAGE) + .build(); + } + + @SuppressWarnings("unused") + static List rejectLoginOnBadArgs() { + return List.of( + Arguments.of(LoginRequest.builder().password("no-name").build(), "system-id-string", IP), + Arguments.of(LOGIN_REQUEST, null, IP)); + } + + @ParameterizedTest + @MethodSource + void rejectLoginOnBadArgs( + final LoginRequest loginRequest, final String systemIdString, final String ip) { + givenNameValidationIsOkay(); + + final LobbyLoginResponse result = loginModule.doLogin(loginRequest, systemIdString, ip); + + assertFailedLogin(result); + } + + private void assertFailedLogin(final LobbyLoginResponse result) { + assertThat(result.getFailReason(), notNullValue()); + assertThat(result.getApiKey(), nullValue()); + verify(userJdbiDao, never()).lookupUserRoleByUserName(any()); + verify(apiKeyGenerator, never()).apply(any()); + verify(accessLogUpdater, never()).accept(any()); + } + + private static void assertSuccessLogin(final LobbyLoginResponse result) { + assertThat(result.getFailReason(), nullValue()); + assertThat(result.getApiKey(), is(API_KEY.getValue())); + assertThat(result.getLobbyMessage(), is(LOBBY_MESSAGE)); + } + + @Nested + class NameValidation { + @Test + void nameIsValidatedOnLogin() { + when(nameValidation.apply(ANONYMOUS_LOGIN_REQUEST.getName())) + .thenReturn(Optional.of("bad-name!")); + + final LobbyLoginResponse result = + loginModule.doLogin(ANONYMOUS_LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertFailedLogin(result); + assertThat(result.getFailReason(), is("bad-name!")); + } + } + + @Nested + class AnonymousLogin { + @Test + void loginRejected() { + givenNameValidationIsOkay(); + when(anonymousLogin.apply(UserName.of(ANONYMOUS_LOGIN_REQUEST.getName()))) + .thenReturn(Optional.of("error")); + + final LobbyLoginResponse result = + loginModule.doLogin(ANONYMOUS_LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertFailedLogin(result); + verify(registeredLogin, never()).test(any()); + verify(tempPasswordLogin, never()).test(any()); + verify(accessLogUpdater, never()).accept(any()); + } + + @Test + void loginSuccess() { + givenNameValidationIsOkay(); + when(anonymousLogin.apply(UserName.of(ANONYMOUS_LOGIN_REQUEST.getName()))) + .thenReturn(Optional.empty()); + when(apiKeyGenerator.apply(any())).thenReturn(API_KEY); + + final LobbyLoginResponse result = + loginModule.doLogin(ANONYMOUS_LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertSuccessLogin(result); + assertThat(result.isPasswordChangeRequired(), is(false)); + verify(registeredLogin, never()).test(any()); + verify(tempPasswordLogin, never()).test(any()); + verify(userJdbiDao, never()).lookupUserRoleByUserName(any()); + final ArgumentCaptor loginRecordArgumentCaptor = + ArgumentCaptor.forClass(LoginRecord.class); + verify(accessLogUpdater).accept(loginRecordArgumentCaptor.capture()); + } + } + + private void givenNameValidationIsOkay() { + when(nameValidation.apply(any())).thenReturn(Optional.empty()); + } + + @Nested + class RegisteredLogin { + @Test + void loginRejected() { + givenNameValidationIsOkay(); + final LobbyLoginResponse result = + loginModule.doLogin(LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertFailedLogin(result); + + verify(anonymousLogin, never()).apply(any()); + verify(accessLogUpdater, never()).accept(any()); + } + + @Test + void loginSuccess() { + givenNameValidationIsOkay(); + when(registeredLogin.test(LOGIN_REQUEST)).thenReturn(true); + when(userJdbiDao.lookupUserRoleByUserName(LOGIN_REQUEST.getName())) + .thenReturn(Optional.of(UserRole.PLAYER)); + when(apiKeyGenerator.apply(any())).thenReturn(API_KEY); + + final LobbyLoginResponse result = + loginModule.doLogin(LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertSuccessLogin(result); + assertThat(result.isPasswordChangeRequired(), is(false)); + verify(anonymousLogin, never()).apply(any()); + final ArgumentCaptor loginRecordArgumentCaptor = + ArgumentCaptor.forClass(LoginRecord.class); + verify(accessLogUpdater).accept(loginRecordArgumentCaptor.capture()); + } + } + + @Nested + class TempPasswordLogin { + @Test + void loginSuccess() { + givenNameValidationIsOkay(); + when(tempPasswordLogin.test(LOGIN_REQUEST)).thenReturn(true); + when(userJdbiDao.lookupUserRoleByUserName(LOGIN_REQUEST.getName())) + .thenReturn(Optional.of(UserRole.PLAYER)); + when(apiKeyGenerator.apply(any())).thenReturn(API_KEY); + + final LobbyLoginResponse result = + loginModule.doLogin(LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertSuccessLogin(result); + assertThat(result.isPasswordChangeRequired(), is(true)); + verify(anonymousLogin, never()).apply(any()); + verify(accessLogUpdater).accept(any()); + } + } + + /** Verify lobby login result 'isModerator' flag has expected values. */ + @Nested + class ModeratorUser { + @ParameterizedTest + @ValueSource(strings = {UserRole.MODERATOR, UserRole.ADMIN}) + void loginSuccessWithModerator(final String moderatorUserRole) { + givenNameValidationIsOkay(); + givenLoginWithUserRole(moderatorUserRole); + + final LobbyLoginResponse result = + loginModule.doLogin(LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertThat(result.isModerator(), is(true)); + } + + private void givenLoginWithUserRole(final String userRole) { + when(registeredLogin.test(LOGIN_REQUEST)).thenReturn(true); + when(userJdbiDao.lookupUserRoleByUserName(LOGIN_REQUEST.getName())) + .thenReturn(Optional.of(userRole)); + when(apiKeyGenerator.apply(any())).thenReturn(API_KEY); + } + + @ParameterizedTest + @ValueSource(strings = {UserRole.ANONYMOUS, UserRole.PLAYER}) + void loginSuccessWithNonModerator(final String nonModeratorUserRole) { + givenNameValidationIsOkay(); + givenLoginWithUserRole(nonModeratorUserRole); + + final LobbyLoginResponse result = + loginModule.doLogin(LOGIN_REQUEST, SYSTEM_ID.getValue(), IP); + + assertThat(result.isModerator(), is(false)); + } + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLoginTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLoginTest.java new file mode 100644 index 0000000..b0d3008 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/anonymous/AnonymousLoginTest.java @@ -0,0 +1,63 @@ +package org.triplea.modules.user.account.login.authorizer.anonymous; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.domain.data.UserName; +import org.triplea.modules.chat.Chatters; + +@ExtendWith(MockitoExtension.class) +class AnonymousLoginTest { + private static final UserName PLAYER_NAME = UserName.of("Player"); + + @Mock private Function> nameIsAvailableValidator; + @Mock private Chatters chatters; + + private AnonymousLogin anonymousLogin; + + @BeforeEach + void setUp() { + anonymousLogin = + AnonymousLogin.builder() + .nameIsAvailableValidation(nameIsAvailableValidator) + .chatters(chatters) + .build(); + } + + @Test + void nameIsInUse() { + when(chatters.isPlayerConnected(PLAYER_NAME)).thenReturn(true); + + final Optional result = anonymousLogin.apply(PLAYER_NAME); + + assertThat(result, isPresent()); + } + + @Test + void nameIsRegistered() { + when(nameIsAvailableValidator.apply(PLAYER_NAME.getValue())).thenReturn(Optional.of("present")); + + final Optional result = anonymousLogin.apply(PLAYER_NAME); + + assertThat(result, isPresent()); + } + + @Test + void allowLogin() { + when(chatters.isPlayerConnected(PLAYER_NAME)).thenReturn(false); + when(nameIsAvailableValidator.apply(PLAYER_NAME.getValue())).thenReturn(Optional.empty()); + + final Optional result = anonymousLogin.apply(PLAYER_NAME); + + assertThat(result, isEmpty()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheckTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheckTest.java new file mode 100644 index 0000000..e182075 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/registered/PasswordCheckTest.java @@ -0,0 +1,69 @@ +package org.triplea.modules.user.account.login.authorizer.registered; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.BiPredicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; +import org.triplea.http.client.lobby.login.LoginRequest; + +@ExtendWith(MockitoExtension.class) +class PasswordCheckTest { + private static final String PLAYER_NAME = "player-name"; + private static final String PASSWORD = "plaintext-pass"; + private static final String DB_PASSWORD = "db-pass"; + + @Mock private UserJdbiDao userJdbiDao; + + @Mock private BiPredicate bcryptCheck; + + private PasswordCheck passwordCheck; + + @BeforeEach + void setUp() { + passwordCheck = + PasswordCheck.builder().userJdbiDao(userJdbiDao).passwordVerifier(bcryptCheck).build(); + } + + @Test + void userHasNoPassword() { + when(userJdbiDao.getPassword(PLAYER_NAME)).thenReturn(Optional.empty()); + + final boolean result = + passwordCheck.test(LoginRequest.builder().name(PLAYER_NAME).password(PASSWORD).build()); + + assertThat(result, is(false)); + } + + @Test + void incorrectPassword() { + whenPasswordIsValid(false); + + final boolean result = + passwordCheck.test(LoginRequest.builder().name(PLAYER_NAME).password(PASSWORD).build()); + + assertThat(result, is(false)); + } + + private void whenPasswordIsValid(final boolean isValid) { + when(userJdbiDao.getPassword(PLAYER_NAME)).thenReturn(Optional.of(DB_PASSWORD)); + when(bcryptCheck.test(PASSWORD, DB_PASSWORD)).thenReturn(isValid); + } + + @Test + void correctPassword() { + whenPasswordIsValid(true); + + final boolean result = + passwordCheck.test(LoginRequest.builder().name(PLAYER_NAME).password(PASSWORD).build()); + + assertThat(result, is(true)); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLoginTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLoginTest.java new file mode 100644 index 0000000..c49788e --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/login/authorizer/temp/password/TempPasswordLoginTest.java @@ -0,0 +1,71 @@ +package org.triplea.modules.user.account.login.authorizer.temp.password; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.function.BiPredicate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.temp.password.TempPasswordDao; +import org.triplea.http.client.lobby.login.LoginRequest; + +@ExtendWith(MockitoExtension.class) +class TempPasswordLoginTest { + private static final String TEMP_PASSWORD_FROM_DB = "from-db"; + + private static final LoginRequest LOGIN_REQUEST = + LoginRequest.builder().name("login-name").password("incoming-password").build(); + + @Mock private TempPasswordDao tempPasswordDao; + @Mock private BiPredicate passwordChecker; + + private TempPasswordLogin tempPasswordLogin; + + @BeforeEach + void setUp() { + tempPasswordLogin = + TempPasswordLogin.builder() + .passwordChecker(passwordChecker) + .tempPasswordDao(tempPasswordDao) + .build(); + } + + @Test + void userHasNoTempPassword() { + when(tempPasswordDao.fetchTempPassword(LOGIN_REQUEST.getName())).thenReturn(Optional.empty()); + + final boolean result = tempPasswordLogin.test(LOGIN_REQUEST); + + assertThat(result, is(false)); + } + + @Test + void hasPasswordButMismatches() { + when(tempPasswordDao.fetchTempPassword(LOGIN_REQUEST.getName())) + .thenReturn(Optional.of(TEMP_PASSWORD_FROM_DB)); + when(passwordChecker.test(LOGIN_REQUEST.getPassword(), TEMP_PASSWORD_FROM_DB)) + .thenReturn(false); + + final boolean result = tempPasswordLogin.test(LOGIN_REQUEST); + + assertThat(result, is(false)); + } + + @Test + void tempPasswordMatches() { + when(tempPasswordDao.fetchTempPassword(LOGIN_REQUEST.getName())) + .thenReturn(Optional.of(TEMP_PASSWORD_FROM_DB)); + when(passwordChecker.test(LOGIN_REQUEST.getPassword(), TEMP_PASSWORD_FROM_DB)).thenReturn(true); + + final boolean result = tempPasswordLogin.test(LOGIN_REQUEST); + + assertThat(result, is(true)); + verify(tempPasswordDao).invalidateTempPasswords(LOGIN_REQUEST.getName()); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/modules/user/account/update/UpdateAccountServiceTest.java b/server/lobby-module/src/test/java/org/triplea/modules/user/account/update/UpdateAccountServiceTest.java new file mode 100644 index 0000000..e99ce64 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/modules/user/account/update/UpdateAccountServiceTest.java @@ -0,0 +1,65 @@ +package org.triplea.modules.user.account.update; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.UserJdbiDao; + +@ExtendWith(MockitoExtension.class) +class UpdateAccountServiceTest { + + private static final int USER_ID = 100; + private static final String PASSWORD = "password-value"; + private static final String HASHED_PASSWORD = "hashed-password-value"; + private static final String EMAIL = "email"; + + @Mock private UserJdbiDao userJdbiDao; + @Mock private Function passwordEncrypter; + + private UpdateAccountService userAccountService; + + @BeforeEach + void setUp() { + userAccountService = + UpdateAccountService.builder() + .userJdbiDao(userJdbiDao) + .passwordEncrpter(passwordEncrypter) + .build(); + } + + @Test + void changePassword() { + when(passwordEncrypter.apply(PASSWORD)).thenReturn(HASHED_PASSWORD); + when(userJdbiDao.updatePassword(USER_ID, HASHED_PASSWORD)).thenReturn(1); + + userAccountService.changePassword(USER_ID, PASSWORD); + + verify(userJdbiDao).updatePassword(USER_ID, HASHED_PASSWORD); + } + + @Test + void fetchEmail() { + when(userJdbiDao.fetchEmail(USER_ID)).thenReturn(EMAIL); + + final String result = userAccountService.fetchEmail(USER_ID); + + assertThat(result, is(EMAIL)); + } + + @Test + void changeEmail() { + when(userJdbiDao.updateEmail(USER_ID, EMAIL)).thenReturn(1); + + userAccountService.changeEmail(USER_ID, EMAIL); + + verify(userJdbiDao).updateEmail(USER_ID, EMAIL); + } +} diff --git a/server/lobby-module/src/test/java/org/triplea/web/socket/SessionBannedCheckTest.java b/server/lobby-module/src/test/java/org/triplea/web/socket/SessionBannedCheckTest.java new file mode 100644 index 0000000..d899227 --- /dev/null +++ b/server/lobby-module/src/test/java/org/triplea/web/socket/SessionBannedCheckTest.java @@ -0,0 +1,39 @@ +package org.triplea.web.socket; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.triplea.db.dao.user.ban.UserBanDao; +import org.triplea.java.IpAddressParser; + +@ExtendWith(MockitoExtension.class) +class SessionBannedCheckTest { + + @Mock private UserBanDao userBanDao; + + @InjectMocks private SessionBannedCheck sessionBannedCheck; + + @Test + void notBanned() { + givenSessionIsBanned(false, "1.1.1.1"); + + assertThat(sessionBannedCheck.test(IpAddressParser.fromString("1.1.1.1")), is(false)); + } + + @Test + void banned() { + givenSessionIsBanned(true, "1.1.1.1"); + + assertThat(sessionBannedCheck.test(IpAddressParser.fromString("1.1.1.1")), is(true)); + } + + private void givenSessionIsBanned(final boolean isBanned, final String ipAddress) { + when(userBanDao.isBannedByIp(ipAddress)).thenReturn(isBanned); + } +} diff --git a/server/lobby-module/src/test/resources/datasets/access_log/access_log.yml b/server/lobby-module/src/test/resources/datasets/access_log/access_log.yml new file mode 100644 index 0000000..bc1d9d3 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/access_log/access_log.yml @@ -0,0 +1,11 @@ +access_log: + - access_time: 2016-01-01 23:59:20.0 + username: first + ip: 127.0.0.1 + system_id: system-id1 + lobby_user_id: 6424 + - access_time: 2016-01-03 23:59:20.0 + username: second + ip: 127.0.0.2 + system_id: system-id2 + lobby_user_id: null diff --git a/server/lobby-module/src/test/resources/datasets/access_log/access_log_post_insert.yml b/server/lobby-module/src/test/resources/datasets/access_log/access_log_post_insert.yml new file mode 100644 index 0000000..b03e4a3 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/access_log/access_log_post_insert.yml @@ -0,0 +1,9 @@ +access_log: + - username: anonymous + ip: 127.0.0.50 + system_id: anonymous-system-id + lobby_user_id: null + - username: registered_user + ip: 127.0.0.20 + system_id: registered-system-id + lobby_user_id: 6424 diff --git a/server/lobby-module/src/test/resources/datasets/access_log/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/access_log/lobby_user.yml new file mode 100644 index 0000000..e227832 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/access_log/lobby_user.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 6424 + user_role_id: 9111 + username: registered_user + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + email: email@ diff --git a/server/lobby-module/src/test/resources/datasets/access_log/user_role.yml b/server/lobby-module/src/test/resources/datasets/access_log/user_role.yml new file mode 100644 index 0000000..855b8ff --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/access_log/user_role.yml @@ -0,0 +1,3 @@ +user_role: + - id: 9111 + name: PLAYER diff --git a/server/lobby-module/src/test/resources/datasets/bad_words/bad_word.yml b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word.yml new file mode 100644 index 0000000..f18b7c8 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word.yml @@ -0,0 +1,9 @@ +bad_word: + - word: "one" + date_created: 2016-01-03 23:59:20.0 + - word: "zzz" + date_created: 2016-01-03 23:59:20.0 + - word: "two" + date_created: 2016-01-03 23:59:20.0 + - word: "aaa" + date_created: 2016-01-03 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_insert.yml b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_insert.yml new file mode 100644 index 0000000..188a09e --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_insert.yml @@ -0,0 +1,6 @@ +bad_word: + - word: "aaa" + - word: "new-bad-word" + - word: "one" + - word: "two" + - word: "zzz" diff --git a/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_remove.yml b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_remove.yml new file mode 100644 index 0000000..0e7e4f5 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/bad_words/bad_word_post_remove.yml @@ -0,0 +1 @@ +bad_word: diff --git a/server/lobby-module/src/test/resources/datasets/game_chat_history/game_chat_history.yml b/server/lobby-module/src/test/resources/datasets/game_chat_history/game_chat_history.yml new file mode 100644 index 0000000..39957a0 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_chat_history/game_chat_history.yml @@ -0,0 +1,41 @@ +game_chat_history: + - id: 500 + lobby_game_id: 6000 + date: 2100-01-01 23:00:20.0 + username: "player1" + message: "Hello good sir" + - id: 501 + lobby_game_id: 6000 + date: 2100-01-01 23:01:20.0 + username: "sir_hosts_a_lot" + message: "Why hello to you" + - id: 502 + lobby_game_id: 6000 + date: 2100-01-01 23:02:20.0 + username: "player1" + message: "What a fine day it is my good sir" + - id: 503 + lobby_game_id: 6000 + date: 2100-01-01 23:03:20.0 + username: "sir_hosts_a_lot" + message: "What a fine day it is indeed!" + - id: 504 + lobby_game_id: 6002 + date: 2100-01-01 23:01:20.0 + username: "sir_hosts_a_little" + message: "hello!" + - id: 505 + lobby_game_id: 6002 + date: 2100-01-01 23:02:20.0 + username: "sir_hosts_a_lot" + message: "join my game?" + - id: 506 + lobby_game_id: 6002 + date: 2100-01-01 23:03:20.0 + username: "sir_hosts_a_little" + message: "Maybe another day" + - id: 507 + lobby_game_id: 7000 + date: 2000-01-01 23:03:20.0 + username: "sir_hosts_a_little" + message: "this is a very old message" diff --git a/server/lobby-module/src/test/resources/datasets/game_chat_history/game_hosting_api_key.yml b/server/lobby-module/src/test/resources/datasets/game_chat_history/game_hosting_api_key.yml new file mode 100644 index 0000000..23600c3 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_chat_history/game_hosting_api_key.yml @@ -0,0 +1,9 @@ +game_hosting_api_key: + - id: 70000 + key: "key0" + ip: "1.1.1.1" + date_created: 2000-01-01 23:59:20.0 + - id: 70001 + key: "key1" + ip: "1.1.1.1" + date_created: 2000-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/game_chat_history/lobby_game.yml b/server/lobby-module/src/test/resources/datasets/game_chat_history/lobby_game.yml new file mode 100644 index 0000000..d9bcb88 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_chat_history/lobby_game.yml @@ -0,0 +1,22 @@ +lobby_game: + - id: 6000 + host_name: "sir_hosts_a_lot" + game_id: "game-hosts-a-lot" + game_hosting_api_key_id: 70000 + date_created: 2000-01-01 23:59:20.0 + - id: 6001 + host_name: "sir_hosts_a_lot" + game_id: "game-empty-chat" + game_hosting_api_key_id: 70000 + date_created: 2000-01-01 23:59:20.0 + - id: 6002 + host_name: "sir_hosts_a_little" + game_id: "game-hosts-a-little" + game_hosting_api_key_id: 70000 + date_created: 2000-01-01 23:59:20.0 + - id: 7000 + host_name: "sir_hosts_a_little" + game_id: "game-far-past" + game_hosting_api_key_id: 70001 + date_created: 2001-01-01 23:59:20.0 + diff --git a/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_after.yml b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_after.yml new file mode 100644 index 0000000..9c678b1 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_after.yml @@ -0,0 +1,3 @@ +game_hosting_api_key: + - key: game-hosting-api-key + ip: 127.0.0.2 diff --git a/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_before.yml b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_before.yml new file mode 100644 index 0000000..6d5eef9 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/insert_key_before.yml @@ -0,0 +1 @@ +game_hosting_api_key: diff --git a/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/key_exists.yml b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/key_exists.yml new file mode 100644 index 0000000..18d4bbe --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/game_hosting_api_key/key_exists.yml @@ -0,0 +1,5 @@ +game_hosting_api_key: + - id: 1 + key: game-hosting-key + ip: 127.0.0.1 + date_created: 2100-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_after.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_after.yml new file mode 100644 index 0000000..ad8028a --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_after.yml @@ -0,0 +1,10 @@ +lobby_api_key: + - id: 1 + username: user-name + lobby_user_id: + user_role_id: 1 + player_chat_id: player-chat-id + key: registered-user-key + system_id: system-id + ip: 127.0.0.1 + date_created: 2100-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_before.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_before.yml new file mode 100644 index 0000000..0094f3f --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/delete_old_keys_before.yml @@ -0,0 +1,19 @@ +lobby_api_key: + - id: 1 + username: user-name + lobby_user_id: + user_role_id: 1 + player_chat_id: player-chat-id + key: registered-user-key + system_id: system-id + ip: 127.0.0.1 + date_created: 2100-01-01 23:59:20.0 + - id: 2 + username: user-name + lobby_user_id: + user_role_id: 1 + player_chat_id: player-chat-id2 + key: registered-user-key2 + system_id: system-id2 + ip: 127.0.0.1 + date_created: 2000-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/empty_lobby_api_key.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/empty_lobby_api_key.yml new file mode 100644 index 0000000..a5c4c32 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/empty_lobby_api_key.yml @@ -0,0 +1 @@ +lobby_api_key: diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key.yml new file mode 100644 index 0000000..aa04646 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key.yml @@ -0,0 +1,26 @@ +lobby_api_key: + - id: 1000 + username: registered-user + lobby_user_id: 50 + user_role_id: 1 + key: zapi-key1 + player_chat_id: chat-id0 + system_id: system-id0 + ip: 127.0.0.1 + date_created: 2000-01-01 23:59:20.0 + - id: 1001 + key: zapi-key2 + username: some-other-name + user_role_id: 3 + player_chat_id: chat-id1 + system_id: system-id1 + ip: 127.0.0.1 + date_created: 2100-01-01 23:59:20.0 + - id: 1002 + key: zapi-key3 + username: user-name + user_role_id: 4 + player_chat_id: chat-id2 + system_id: system-id2 + ip: 127.0.0.1 + date_created: 2100-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key_post_insert.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key_post_insert.yml new file mode 100644 index 0000000..d680512 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_api_key_post_insert.yml @@ -0,0 +1,8 @@ +lobby_api_key: + - username: registered-user-name + lobby_user_id: 50 + user_role_id: 1 + player_chat_id: player-chat-id + key: registered-user-key + system_id: system-id + ip: 127.0.0.1 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_user.yml new file mode 100644 index 0000000..518ab96 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/lobby_user.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 50 + user_role_id: 1 + username: existing-name + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + email: email@ diff --git a/server/lobby-module/src/test/resources/datasets/lobby_api_key/user_role.yml b/server/lobby-module/src/test/resources/datasets/lobby_api_key/user_role.yml new file mode 100644 index 0000000..6505962 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_api_key/user_role.yml @@ -0,0 +1,9 @@ +user_role: + - id: 1 + name: MODERATOR + - id: 2 + name: PLAYER + - id: 3 + name: ANONYMOUS + - id: 4 + name: HOST diff --git a/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_api_key.yml b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_api_key.yml new file mode 100644 index 0000000..d6c742c --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_api_key.yml @@ -0,0 +1,12 @@ + + +lobby_api_key: + - id: 3000 + username: user-name + lobby_user_id: 50 + user_role_id: 324 + key: zapi-key1 + player_chat_id: chat-id0 + system_id: system-id0 + ip: 127.0.0.1 + date_created: 2000-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_chat_history_post_insert.yml b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_chat_history_post_insert.yml new file mode 100644 index 0000000..5273f34 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_chat_history_post_insert.yml @@ -0,0 +1,4 @@ +lobby_chat_history: + - username: "username" + lobby_api_key_id: 3000 + message: "message" diff --git a/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_user.yml new file mode 100644 index 0000000..4208a66 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/lobby_user.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 50 + user_role_id: 324 + username: existing-name + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + email: email@ diff --git a/server/lobby-module/src/test/resources/datasets/lobby_chat_history/user_role.yml b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/user_role.yml new file mode 100644 index 0000000..490bb7d --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_chat_history/user_role.yml @@ -0,0 +1,3 @@ +user_role: + - id: 324 + name: PLAYER diff --git a/server/lobby-module/src/test/resources/datasets/lobby_games/game_chat_history_post_insert.yml b/server/lobby-module/src/test/resources/datasets/lobby_games/game_chat_history_post_insert.yml new file mode 100644 index 0000000..69964e4 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_games/game_chat_history_post_insert.yml @@ -0,0 +1,4 @@ +game_chat_history: + - lobby_game_id: 100 + username: "gameplayer" + message: "example message" diff --git a/server/lobby-module/src/test/resources/datasets/lobby_games/game_hosting_api_key.yml b/server/lobby-module/src/test/resources/datasets/lobby_games/game_hosting_api_key.yml new file mode 100644 index 0000000..a6ee471 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_games/game_hosting_api_key.yml @@ -0,0 +1,10 @@ +game_hosting_api_key: + - id: 600 + key: "abc" + ip: "127.0.0.1" + date_created: 2000-01-01 23:59:20.0 + - id: 1200 + key: 06dbbb9b6ac87d97f9acca120ae4784d0eaf6865ea99788a389a384da8ab0709e77af2bfe4f4e82c6e6d375ae256aa95c2fa99ce97ce65981cfd1340257a441a + # key = sha512(HOST) + ip: "127.0.0.1" + date_created: 2000-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game.yml b/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game.yml new file mode 100644 index 0000000..6b1b9cf --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game.yml @@ -0,0 +1,6 @@ +lobby_game: + - id: 100 + host_name: "hostname" + game_id: "gameid-100" + game_hosting_api_key_id: 600 + date_created: 2000-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game_post_insert.yml b/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game_post_insert.yml new file mode 100644 index 0000000..60359da --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/lobby_games/lobby_game_post_insert.yml @@ -0,0 +1,4 @@ +lobby_game: + - host_name: "name" + game_id: "game-id" + game_hosting_api_key_id: 1200 diff --git a/server/lobby-module/src/test/resources/datasets/moderator_audit/empty_moderator_action_history.yml b/server/lobby-module/src/test/resources/datasets/moderator_audit/empty_moderator_action_history.yml new file mode 100644 index 0000000..3015eaf --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_audit/empty_moderator_action_history.yml @@ -0,0 +1 @@ +moderator_action_history: diff --git a/server/lobby-module/src/test/resources/datasets/moderator_audit/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/moderator_audit/lobby_user.yml new file mode 100644 index 0000000..218e476 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_audit/lobby_user.yml @@ -0,0 +1,12 @@ +lobby_user: + - id: 800000 + username: moderator1 + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 + - id: 900000 + username: moderator2 + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 + diff --git a/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history.yml b/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history.yml new file mode 100644 index 0000000..35f0020 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history.yml @@ -0,0 +1,21 @@ +moderator_action_history: + - lobby_user_id: 800000 + action_name: "BAN_USERNAME" + action_target: "ACTION_TARGET1" + date_created: 2016-01-01 23:59:20.0 + - lobby_user_id: 900000 + action_name: "MUTE_USERNAME" + action_target: "ACTION_TARGET2" + date_created: 2016-01-02 23:59:20.0 + - lobby_user_id: 900000 + action_name: "BAN_USERNAME" + action_target: "ACTION_TARGET3" + date_created: 2016-01-03 23:59:20.0 + - lobby_user_id: 800000 + action_name: "BOOT_PLAYER" + action_target: "ACTION_TARGET4" + date_created: 2016-01-04 23:59:20.0 + - lobby_user_id: 900000 + action_name: "BAN_USERNAME" + action_target: "ACTION_TARGET5" + date_created: 2016-01-05 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history_post_insert.yml b/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history_post_insert.yml new file mode 100644 index 0000000..25128cd --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_audit/moderator_action_history_post_insert.yml @@ -0,0 +1,5 @@ +moderator_action_history: + - id: "regex:\\d+" #any number + lobby_user_id: 900000 + action_name: "BAN_USERNAME" + action_target: "ACTION_TARGET" diff --git a/server/lobby-module/src/test/resources/datasets/moderator_audit/user_role.yml b/server/lobby-module/src/test/resources/datasets/moderator_audit/user_role.yml new file mode 100644 index 0000000..ec17070 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_audit/user_role.yml @@ -0,0 +1,3 @@ +user_role: + - id: 1 + name: MODERATOR diff --git a/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/access_log.yml b/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/access_log.yml new file mode 100644 index 0000000..28b98a4 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/access_log.yml @@ -0,0 +1,31 @@ +access_log: + - access_time: 2151-01-01 23:59:20.0 + username: name1 + ip: 1.1.1.1 + system_id: system-id + lobby_user_id: null + - access_time: 2152-01-01 23:59:20.0 + username: name2 + ip: 1.1.1.1 + system_id: system-id2 + lobby_user_id: null + - access_time: 2153-01-01 23:59:20.0 + username: name3 + ip: 2.2.2.2 + system_id: system-id + lobby_user_id: null + - access_time: 2154-01-01 23:59:20.0 + username: name3 + ip: 2.2.2.2 + system_id: system-id + lobby_user_id: null + - access_time: 2000-01-01 23:59:20.0 + username: far-past-is-filtered + ip: 2.2.2.2 + system_id: system-id + lobby_user_id: null + - access_time: 2150-01-01 23:59:20.0 + username: this-record-matches-no-query + ip: 5.5.5.5 + system_id: some-other-system-id + lobby_user_id: null diff --git a/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/banned_user.yml b/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/banned_user.yml new file mode 100644 index 0000000..97e1b93 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderator_player_lookup/banned_user.yml @@ -0,0 +1,29 @@ +banned_user: + - id: 3000 + public_id: xyz + username: name1 + system_id: system-id + ip: 1.1.1.1 + date_created: 2010-01-01 23:59:20.0 + ban_expiry: 2100-01-01 23:59:20.0 + - id: 3001 + public_id: xyz2 + username: name2 + system_id: system-id + ip: 2.2.2.2 + date_created: 2000-01-01 23:59:20.0 + ban_expiry: 2010-01-01 23:59:20.0 + - id: 3002 + public_id: xyz3 + username: name2 + system_id: system-id2 + ip: 1.1.1.1 + date_created: 2000-01-01 23:59:20.0 + ban_expiry: 2050-01-01 23:59:20.0 + - id: 5000 + public_id: does-not-match-any-query + username: name32 + system_id: system-id-no-match + ip: 2.2.1.3 + date_created: 2000-01-01 23:59:20.0 + ban_expiry: 2050-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/moderators/access_log.yml b/server/lobby-module/src/test/resources/datasets/moderators/access_log.yml new file mode 100644 index 0000000..f42c454 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderators/access_log.yml @@ -0,0 +1,6 @@ +access_log: + - username: registered + ip: 127.0.0.20 + system_id: registered-system-id + lobby_user_id: 900000 + access_time: 2001-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/moderators/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/moderators/lobby_user.yml new file mode 100644 index 0000000..8657f69 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderators/lobby_user.yml @@ -0,0 +1,16 @@ +lobby_user: + - id: 100000 + username: not moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 + - id: 900000 + username: moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 2 + - id: 900001 + username: Super! moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 3 diff --git a/server/lobby-module/src/test/resources/datasets/moderators/lobby_user_post_update_roles.yml b/server/lobby-module/src/test/resources/datasets/moderators/lobby_user_post_update_roles.yml new file mode 100644 index 0000000..bc12c71 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderators/lobby_user_post_update_roles.yml @@ -0,0 +1,16 @@ +lobby_user: + - id: 100000 + username: not moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 2 + - id: 900000 + username: moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 3 + - id: 900001 + username: Super! moderator + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/moderators/user_role.yml b/server/lobby-module/src/test/resources/datasets/moderators/user_role.yml new file mode 100644 index 0000000..020b121 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/moderators/user_role.yml @@ -0,0 +1,7 @@ +user_role: + - id: 1 + name: PLAYER + - id: 2 + name: MODERATOR + - id: 3 + name: ADMIN diff --git a/server/lobby-module/src/test/resources/datasets/temp_password/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/temp_password/lobby_user.yml new file mode 100644 index 0000000..c8faa14 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/temp_password/lobby_user.yml @@ -0,0 +1,11 @@ +lobby_user: + - id: 500000 + username: username + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 + - id: 1 + username: user-with-temp-password + bcrypt_password: $2a$01234_123456789_123456789_123456789_123456789_123456700_ + email: email@ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/temp_password/temp_password_request.yml b/server/lobby-module/src/test/resources/datasets/temp_password/temp_password_request.yml new file mode 100644 index 0000000..aac9bb1 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/temp_password/temp_password_request.yml @@ -0,0 +1,11 @@ +temp_password_request: + - lobby_user_id: 500000 + temp_password: invalid + date_created: 2016-01-01 23:59:20.0 + date_invalidated: 2016-01-02 23:59:20.0 + - lobby_user_id: 500000 + temp_password: temp + date_created: 2016-01-01 23:59:20.0 + - lobby_user_id: 1 + temp_password: temp + date_created: 2050-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/temp_password/user_role.yml b/server/lobby-module/src/test/resources/datasets/temp_password/user_role.yml new file mode 100644 index 0000000..654cbee --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/temp_password/user_role.yml @@ -0,0 +1,3 @@ +user_role: + - id: 1 + name: PLAYER diff --git a/server/lobby-module/src/test/resources/datasets/temp_password_history/sample.yml b/server/lobby-module/src/test/resources/datasets/temp_password_history/sample.yml new file mode 100644 index 0000000..c9c29ce --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/temp_password_history/sample.yml @@ -0,0 +1,5 @@ +temp_password_request_history: + - id: 4000 + inetaddress: 127.0.0.1 + username: user + date_created: 2016-01-01 23:59:20.0 diff --git a/server/lobby-module/src/test/resources/datasets/user/change_password_after.yml b/server/lobby-module/src/test/resources/datasets/user/change_password_after.yml new file mode 100644 index 0000000..e8f8751 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/change_password_after.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 900000 + username: user + bcrypt_password: $2a$abcde_123456789_123456789_123456789_123456789_123456789_ + email: email@ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/user/create_user_after.yml b/server/lobby-module/src/test/resources/datasets/user/create_user_after.yml new file mode 100644 index 0000000..8b2d634 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/create_user_after.yml @@ -0,0 +1,5 @@ +lobby_user: + - username: new-user + email: user@email.com + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/user/empty_lobby_user.yml b/server/lobby-module/src/test/resources/datasets/user/empty_lobby_user.yml new file mode 100644 index 0000000..9aba104 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/empty_lobby_user.yml @@ -0,0 +1 @@ +lobby_user: diff --git a/server/lobby-module/src/test/resources/datasets/user/lobby_user.yml b/server/lobby-module/src/test/resources/datasets/user/lobby_user.yml new file mode 100644 index 0000000..6ba2dbe --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/lobby_user.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 900000 + username: user + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + email: email@ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/user/post_change_email.yml b/server/lobby-module/src/test/resources/datasets/user/post_change_email.yml new file mode 100644 index 0000000..e058aae --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/post_change_email.yml @@ -0,0 +1,6 @@ +lobby_user: + - id: 900000 + username: user + bcrypt_password: $2a$56789_123456789_123456789_123456789_123456789_123456789_ + email: new-email@ + user_role_id: 1 diff --git a/server/lobby-module/src/test/resources/datasets/user/user_role.yml b/server/lobby-module/src/test/resources/datasets/user/user_role.yml new file mode 100644 index 0000000..654cbee --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user/user_role.yml @@ -0,0 +1,3 @@ +user_role: + - id: 1 + name: PLAYER diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_after.yml b/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_after.yml new file mode 100644 index 0000000..686e280 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_after.yml @@ -0,0 +1,5 @@ +banned_user: + - public_id: public-id + username: username + system_id: system-id + ip: 127.0.0.3 diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_before.yml b/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_before.yml new file mode 100644 index 0000000..319cfef --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/add_ban_before.yml @@ -0,0 +1 @@ +banned_user: diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/banned_by_ip.yml b/server/lobby-module/src/test/resources/datasets/user_ban/banned_by_ip.yml new file mode 100644 index 0000000..251b7d7 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/banned_by_ip.yml @@ -0,0 +1,15 @@ +banned_user: + - id: 1 + public_id: public-id2 + username: username2 + system_id: system-id2 + ip: 127.0.0.1 + date_created: 2020-01-01 23:59:59 + ban_expiry: 2100-01-01 23:59:59 + - id: 2 + public_id: expired-ban + username: username2 + system_id: system-id2 + ip: 127.0.0.2 + date_created: 2000-01-01 23:59:59 + ban_expiry: 2000-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/lookup_bans.yml b/server/lobby-module/src/test/resources/datasets/user_ban/lookup_bans.yml new file mode 100644 index 0000000..91019ec --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/lookup_bans.yml @@ -0,0 +1,22 @@ +banned_user: + - id: 1 + public_id: public-id1 + username: username1 + system_id: system-id1 + ip: 127.0.0.1 + date_created: 2010-01-01 23:59:59 + ban_expiry: 2200-01-01 23:59:59 + - id: 2 + public_id: public-id2 + username: username2 + system_id: system-id2 + ip: 127.0.0.2 + date_created: 2020-01-01 23:59:59 + ban_expiry: 2100-01-01 23:59:59 + - id: 3 + public_id: expired-ban + username: username2 + system_id: system-id2 + ip: 127.0.0.2 + date_created: 2000-01-01 23:59:59 + ban_expiry: 2000-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/lookup_username_by_ban_id.yml b/server/lobby-module/src/test/resources/datasets/user_ban/lookup_username_by_ban_id.yml new file mode 100644 index 0000000..ea911c2 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/lookup_username_by_ban_id.yml @@ -0,0 +1,7 @@ +banned_user: + - id: 1 + public_id: public-id + username: username + system_id: system-id + ip: 127.0.0.3 + ban_expiry: 2100-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_after.yml b/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_after.yml new file mode 100644 index 0000000..0aaf9b8 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_after.yml @@ -0,0 +1 @@ +banned_user: \ No newline at end of file diff --git a/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_before.yml b/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_before.yml new file mode 100644 index 0000000..ea911c2 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_ban/remove_ban_before.yml @@ -0,0 +1,7 @@ +banned_user: + - id: 1 + public_id: public-id + username: username + system_id: system-id + ip: 127.0.0.3 + ban_expiry: 2100-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/datasets/user_role/initial.yml b/server/lobby-module/src/test/resources/datasets/user_role/initial.yml new file mode 100644 index 0000000..54ecb99 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/user_role/initial.yml @@ -0,0 +1,5 @@ +user_role: + - id: 1 + name: ANONYMOUS + - id: 2 + name: HOST diff --git a/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_after.yml b/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_after.yml new file mode 100644 index 0000000..00c0063 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_after.yml @@ -0,0 +1,2 @@ +banned_username: + - username: username diff --git a/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_before.yml b/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_before.yml new file mode 100644 index 0000000..c4bee95 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/username_ban/add_banned_username_before.yml @@ -0,0 +1 @@ +banned_username: diff --git a/server/lobby-module/src/test/resources/datasets/username_ban/get_banned_usernames.yml b/server/lobby-module/src/test/resources/datasets/username_ban/get_banned_usernames.yml new file mode 100644 index 0000000..32f53b8 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/username_ban/get_banned_usernames.yml @@ -0,0 +1,5 @@ +banned_username: + - username: USERNAME1 + date_created: 2001-01-01 23:59:59 + - username: USERNAME2 + date_created: 2000-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_after.yml b/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_after.yml new file mode 100644 index 0000000..c4bee95 --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_after.yml @@ -0,0 +1 @@ +banned_username: diff --git a/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_before.yml b/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_before.yml new file mode 100644 index 0000000..a62decb --- /dev/null +++ b/server/lobby-module/src/test/resources/datasets/username_ban/remove_banned_username_before.yml @@ -0,0 +1,3 @@ +banned_username: + - username: username + date_created: 2001-01-01 23:59:59 diff --git a/server/lobby-module/src/test/resources/db-cleanup.sql b/server/lobby-module/src/test/resources/db-cleanup.sql new file mode 100644 index 0000000..80cd088 --- /dev/null +++ b/server/lobby-module/src/test/resources/db-cleanup.sql @@ -0,0 +1,16 @@ +-- Deletes table data in proper order + +delete from access_log; +delete from bad_word; +delete from banned_user; +delete from banned_username; +delete from game_chat_history; +delete from lobby_game; +delete from game_hosting_api_key; +delete from lobby_chat_history; +delete from lobby_api_key; +delete from moderator_action_history; +delete from temp_password_request; +delete from temp_password_request_history; +delete from lobby_user; +delete from user_role; diff --git a/server/lobby-module/src/test/resources/dbunit.yml b/server/lobby-module/src/test/resources/dbunit.yml new file mode 100644 index 0000000..1f4bedc --- /dev/null +++ b/server/lobby-module/src/test/resources/dbunit.yml @@ -0,0 +1,5 @@ +connectionConfig: + driver: "org.postgresql.Driver" + url: "jdbc:postgresql://localhost:5432/lobby_db" + user: "lobby_user" + password: "lobby" diff --git a/server/lobby-module/src/test/resources/logback-test.xml b/server/lobby-module/src/test/resources/logback-test.xml new file mode 100644 index 0000000..66d0fe6 --- /dev/null +++ b/server/lobby-module/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + %d [%thread] %logger{36} %-5level: %msg%n + + + + + + + + + + + + + + diff --git a/server/server-lib/build.gradle b/server/server-lib/build.gradle new file mode 100644 index 0000000..7ba6f7e --- /dev/null +++ b/server/server-lib/build.gradle @@ -0,0 +1,64 @@ +plugins { + id "java" +} + +ext { + + apacheHttpComponentsVersion = "4.5.14" + awaitilityVersion = "4.2.1" + bcryptVersion = "0.10.2" + caffeineVersion = "3.1.8" + checkstyleVersion = "8.45" + commonsCliVersion = "1.8.0" + commonsCodecVersion = "1.17.0" + commonsIoVersion = "2.16.1" + commonsMathVersion = "3.6.1" + commonsTextVersion = "1.12.0" + databaseRiderVersion = "1.42.0" + dropwizardVersion = "2.1.0" + dropwizardWebsocketsVersion = "1.3.14" + equalsVerifierVersion = "3.16.1" + feignCoreVersion = "13.2.1" + feignGsonVersion = "13.2.1" + javaWebSocketVersion = "1.5.3" + gsonVersion = "2.11.0" + guavaVersion = "33.2.1-jre" + hamcrestJsonVersion = "0.3" + hamcrestOptionalVersion = "2.0.0" + hamcrestVersion = "2.0.0.0" + jacksonDataTypeVersion = "2.17.1" + jakartaMailVersion = "2.0.1" + javaWebsocketVersion = "1.5.3" + javaxActivationVersion = "1.1.1" + jaxbApiVersion = "2.3.1" + jaxbCoreVersion = "4.0.5" + jaxbImplVersion = "4.0.5" + jdbiVersion = "3.45.1" + jetbrainsAnnotationsVersion = "24.1.0" + jlayerVersion = "1.0.1.4" + junitJupiterVersion = "5.10.2" + junitPlatformLauncherVersion = "1.10.2" + logbackClassicVersion = "1.2.11" + mockitoVersion = "5.11.0" + openFeignVersion = "13.2.1" + postgresqlVersion = "42.7.3" + snakeYamlVersion = "2.7" + sonatypeGoodiesPrefsVersion = "2.3.9" + substanceVersion = "4.5.0" + wireMockJunit5Version = "1.3.1" + wireMockVersion = "3.0.1" + xchartVersion = "3.8.8" + xmlUnitCore = "2.10.0" + xmlUnitMatchers = "2.10.0" + +} + +dependencies { + implementation "com.liveperson:dropwizard-websockets:$dropwizardWebsocketsVersion" + implementation "io.dropwizard:dropwizard-auth:$dropwizardVersion" + implementation "io.dropwizard:dropwizard-core:$dropwizardVersion" + implementation "io.dropwizard:dropwizard-jdbi3:$dropwizardVersion" +// implementation project(":lib:java-extras") + + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion" +} diff --git a/server/server-lib/src/main/java/org/triplea/dropwizard/common/AuthenticationConfiguration.java b/server/server-lib/src/main/java/org/triplea/dropwizard/common/AuthenticationConfiguration.java new file mode 100644 index 0000000..aeb1060 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/dropwizard/common/AuthenticationConfiguration.java @@ -0,0 +1,48 @@ +package org.triplea.dropwizard.common; + +import com.codahale.metrics.MetricRegistry; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.Authorizer; +import io.dropwizard.auth.CachingAuthenticator; +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; +import io.dropwizard.setup.Environment; +import java.security.Principal; +import java.time.Duration; +import lombok.experimental.UtilityClass; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; + +@UtilityClass +public class AuthenticationConfiguration { + + /** + * Enables configuration via OAuth token. Endpoints annotated with @RolesAllowed will be activated + * and will require a user to have been a given role during per-request authentication. + */ + public static void enableAuthentication( + final Environment environment, + final MetricRegistry metrics, + final Authenticator authenticator, + final Authorizer authorizer, + final Class principalClass) { + environment + .jersey() + .register( + new AuthDynamicFeature( + new OAuthCredentialAuthFilter.Builder() + .setAuthenticator( + new CachingAuthenticator<>( + metrics, + authenticator, + Caffeine.newBuilder() + .expireAfterAccess(Duration.ofMinutes(10)) + .maximumSize(10000))) + .setAuthorizer(authorizer) + .setPrefix("Bearer") + .buildAuthFilter())); + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(principalClass)); + environment.jersey().register(new RolesAllowedDynamicFeature()); + } +} diff --git a/server/server-lib/src/main/java/org/triplea/dropwizard/common/IllegalArgumentMapper.java b/server/server-lib/src/main/java/org/triplea/dropwizard/common/IllegalArgumentMapper.java new file mode 100644 index 0000000..5766657 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/dropwizard/common/IllegalArgumentMapper.java @@ -0,0 +1,17 @@ +package org.triplea.dropwizard.common; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +/** + * This class is used to convert IllegalArgumentExceptions thrown by http endpoint controllers to + * return HTTP status 400 codes. Without this, those errors would be 500s. + */ +public class IllegalArgumentMapper implements ExceptionMapper { + @Override + public Response toResponse(final IllegalArgumentException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Http 400 - Bad Request: " + exception.getMessage()) + .build(); + } +} diff --git a/server/server-lib/src/main/java/org/triplea/dropwizard/common/IpAddressExtractor.java b/server/server-lib/src/main/java/org/triplea/dropwizard/common/IpAddressExtractor.java new file mode 100644 index 0000000..62174f5 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/dropwizard/common/IpAddressExtractor.java @@ -0,0 +1,23 @@ +package org.triplea.dropwizard.common; + +import javax.servlet.http.HttpServletRequest; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class IpAddressExtractor { + + /** + * Extracts the IP address of the remote address making an HttpServletRequest. + * + *

httpServletRequest.getRemoteAddr() can return a value surrounded by square brackets. This + * method will return that remote addr with square brackets stripped. + * + * @return valid IP address of the remote machine making + */ + public String extractIpAddress(HttpServletRequest httpServletRequest) { + return httpServletRequest + .getRemoteAddr() // + .replaceAll("\\[", "") + .replaceAll("\\]", ""); + } +} diff --git a/server/server-lib/src/main/java/org/triplea/dropwizard/common/JdbiLogging.java b/server/server-lib/src/main/java/org/triplea/dropwizard/common/JdbiLogging.java new file mode 100644 index 0000000..22f9e42 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/dropwizard/common/JdbiLogging.java @@ -0,0 +1,31 @@ +package org.triplea.dropwizard.common; + +import java.sql.SQLException; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.statement.SqlLogger; +import org.jdbi.v3.core.statement.StatementContext; + +@UtilityClass +@Slf4j +public class JdbiLogging { + /** Adds a logger to JDBI that will log SQL statements before they are executed. */ + public static void registerSqlLogger(final Jdbi jdbi) { + jdbi.setSqlLogger( + new SqlLogger() { + @Override + public void logBeforeExecution(final StatementContext context) { + log.info("Executing SQL: " + context.getRawSql()); + } + + @Override + public void logAfterExecution(final StatementContext context) {} + + @Override + public void logException(final StatementContext context, final SQLException ex) { + log.error("Exception executing SQL: " + context.getRawSql(), ex); + } + }); + } +} diff --git a/server/server-lib/src/main/java/org/triplea/dropwizard/common/ServerConfiguration.java b/server/server-lib/src/main/java/org/triplea/dropwizard/common/ServerConfiguration.java new file mode 100644 index 0000000..5d3848b --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/dropwizard/common/ServerConfiguration.java @@ -0,0 +1,94 @@ +package org.triplea.dropwizard.common; + +import io.dropwizard.Configuration; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.jdbi3.bundles.JdbiExceptionsBundle; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.dropwizard.websockets.WebsocketBundle; +import java.util.Arrays; +import java.util.List; +import javax.websocket.server.ServerEndpointConfig; +import javax.ws.rs.container.ContainerRequestFilter; +import lombok.AllArgsConstructor; + +/** + * Facilitates configuration for a dropwizard server Application class. + * + * @param Configuration class type of the server. + */ +public class ServerConfiguration { + + private final Bootstrap bootstrap; + + @AllArgsConstructor + public static class WebsocketConfig { + private final Class websocketClass; + private final String path; + } + + private ServerConfiguration( + final Bootstrap bootstrap, final WebsocketConfig... websocketConfigs) { + this.bootstrap = bootstrap; + + final ServerEndpointConfig[] websockets = addWebsockets(websocketConfigs); + bootstrap.addBundle(new WebsocketBundle(websockets)); + } + + private ServerEndpointConfig[] addWebsockets(final WebsocketConfig... websocketConfigs) { + return Arrays.stream(websocketConfigs) + .map( + websocketConfig -> + ServerEndpointConfig.Builder.create( + websocketConfig.websocketClass, websocketConfig.path) + .build()) + .toArray(ServerEndpointConfig[]::new); + } + + public static ServerConfiguration build( + final Bootstrap bootstrap, final WebsocketConfig... websocketConfigs) { + return new ServerConfiguration<>(bootstrap, websocketConfigs); + } + + /** + * This bootstrap will replace ${...} values in YML configuration with environment variable + * values. Without it, all values in the YML configuration are treated as literals. + */ + public ServerConfiguration enableEnvironmentVariablesInConfig() { + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider( + bootstrap.getConfigurationSourceProvider(), new EnvironmentVariableSubstitutor(false))); + return this; + } + + /** + * From: https://www.dropwizard.io/0.7.1/docs/manual/jdbi.html By adding the JdbiExceptionsBundle + * to your application, Dropwizard will automatically unwrap ant thrown SQLException or + * DBIException instances. This is critical for debugging, since otherwise only the common wrapper + * exception’s stack trace is logged. + */ + public ServerConfiguration enableBetterJdbiExceptions() { + bootstrap.addBundle(new JdbiExceptionsBundle()); + return this; + } + + public ServerConfiguration registerRequestFilter( + final Environment environment, final ContainerRequestFilter containerRequestFilter) { + environment.jersey().register(containerRequestFilter); + return this; + } + + /** + * Registers an exception mapping, meaning an uncaught exception matching an exception mapper will + * then "go through" the exception mapper. This can be used for example to register an exception + * mapper for something like IllegalArgumentException to return a status 400 response + * rather than a status 500 response. Exception mappers can be also be used for common logging or + * for returning a specific response entity. + */ + public ServerConfiguration registerExceptionMappers( + final Environment environment, final List exceptionMappers) { + exceptionMappers.forEach(mapper -> environment.jersey().register(mapper)); + return this; + } +} diff --git a/server/server-lib/src/main/java/org/triplea/server/lib/scheduled/tasks/ScheduledTask.java b/server/server-lib/src/main/java/org/triplea/server/lib/scheduled/tasks/ScheduledTask.java new file mode 100644 index 0000000..b86ce10 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/server/lib/scheduled/tasks/ScheduledTask.java @@ -0,0 +1,47 @@ +package org.triplea.server.lib.scheduled.tasks; + +import io.dropwizard.lifecycle.Managed; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.triplea.java.timer.ScheduledTimer; +import org.triplea.java.timer.Timers; + +/** + * Use to run a background task on a recurring basis. The task is stopped when the server is + * stopped. Typically used for things like periodic indexing jobs. + */ +@Slf4j +public class ScheduledTask implements Managed { + + private final String taskName; + private final ScheduledTimer taskTimer; + + @Builder + ScheduledTask( + @Nonnull final String taskName, + @Nonnull final Duration period, + @Nonnull final Duration delay, + @Nonnull final Runnable task) { + this.taskName = taskName; + taskTimer = + Timers.fixedRateTimer(taskName) + .period(period.toSeconds(), TimeUnit.SECONDS) + .delay(delay.toSeconds(), TimeUnit.SECONDS) + .task(task); + } + + @Override + public void start() { + log.info("Starting scheduled task: {}", taskName); + taskTimer.start(); + } + + @Override + public void stop() { + log.info("Stopping scheduled task: {}", taskName); + taskTimer.cancel(); + } +} diff --git a/server/server-lib/src/main/java/org/triplea/spitfire/server/HttpController.java b/server/server-lib/src/main/java/org/triplea/spitfire/server/HttpController.java new file mode 100644 index 0000000..475e8f8 --- /dev/null +++ b/server/server-lib/src/main/java/org/triplea/spitfire/server/HttpController.java @@ -0,0 +1,17 @@ +package org.triplea.spitfire.server; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * Base class for http server controllers. This class is mainly to share the annotations needed for + * enabling an http controller. All http controller classes should be 'registered' in the + * application configuration, {@see ServerApplication} + */ +@Path("/") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SuppressWarnings("RestResourceMethodInspection") +public class HttpController {} diff --git a/server/server-lib/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java b/server/server-lib/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java new file mode 100644 index 0000000..086d6e0 --- /dev/null +++ b/server/server-lib/src/test/java/org/triplea/dropwizard/common/IpAddressExtractorTest.java @@ -0,0 +1,47 @@ +package org.triplea.dropwizard.common; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class IpAddressExtractorTest { + + private static final String IPV4 = "127.0.0.1"; + private static final String IPV6 = "3ffe:1900:4545:3:200:f8ff:fe21:67cf"; + + @Mock private HttpServletRequest httpServletRequest; + + @Test + void ipv6() { + when(httpServletRequest.getRemoteAddr()).thenReturn(IPV6); + assertThat( + "Normal formatted IPv6 as input should return same value", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV6)); + } + + @Test + void ipv6_WithBrackets() { + when(httpServletRequest.getRemoteAddr()).thenReturn("[" + IPV6 + "]"); + assertThat( + "Square brackets on the IPv6 should be stripped", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV6)); + } + + @Test + void ipv4() { + when(httpServletRequest.getRemoteAddr()).thenReturn(IPV4); + assertThat( + "IPv4 format is returned unaltered", + IpAddressExtractor.extractIpAddress(httpServletRequest), + is(IPV4)); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d6584e1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +rootProject.name='lobby-server' +//include 'lib:feign-common' +//include 'lib:http-client-lib' +//include 'lib:java-extras' +//include 'lib:swing-lib' +//include 'lib:swing-lib-test-support' +//include 'lib:test-common' +//include 'lib:websocket-client' +//include 'lib:websocket-server' +//include 'lib:xml-reader' + +include 'http-clients:github-client' +include 'server:server-lib' +include 'server:database' +include 'server:database-test-support' +include 'server:dropwizard-server' +include 'server:lobby-module' diff --git a/src/main/java/org/triplea/Example.java b/src/main/java/org/triplea/Example.java deleted file mode 100644 index 77b5346..0000000 --- a/src/main/java/org/triplea/Example.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.triplea; - -import org.triplea.http.client.error.report.CanUploadErrorReportResponse; - -/** - * Just some sample code that uses the JAR dependency on triplea-game/triplea. This is making sure - * we can use code from our very own triplea-game maven repository. - */ -public class Example { - public static void main(String[] args) { - CanUploadErrorReportResponse canUploadErrorReportResponse; - } -} diff --git a/verify.sh b/verify.sh index 15712c7..0dd0f50 100755 --- a/verify.sh +++ b/verify.sh @@ -1,7 +1,51 @@ #!/bin/bash -scriptDir="$(dirname "$0")" +# This script runs all checks across the entire project. + +# Checks if a dependency is installed on a system. +function checkDependency { + local depName="$1" + if hash "$depName"; then + return 0 + else + echo "ERROR: dependency not installed '$depName'" + return 1 + fi +} + +# Installs Docker and dependencies +# Docker is installed through python, we install python & pip first, then docker. +# Last, we add current user to the 'docker' group to enable sudo-less docker. +function installDocker { + if [ "$(uname)" == "Darwin" ]; then + echo "Follow these steps to install docker: https://store.docker.com/editions/community/docker-ce-desktop-mac" + exit 1 + fi + echo "Installing python and pip, a dependency of docker" + if hash yum; then + set -x + sudo yum update -y + sudo yum install -y python3 python3-pip + set +x + else + set -x + sudo apt install -y python3 python3-pip + set +x + fi + echo "Installing Docker (with pip)" + set -x + pip3 install docker docker-compose + set +x + + echo "Adding current user: $USER to group: 'docker' (allows sudo-less docker)." + echo "Log back in for these changes to take effect." + groups | grep -q "docker" || sudo usermod -a -G docker "$USER" +} + +scriptDir="$(dirname $0)" set -o pipefail set -eu +checkDependency "docker" || installDocker "$scriptDir/gradlew" spotlessApply check $@ +"$scriptDir/.build/code-convention-checks/check-custom-style"