From 8b09eb7fb5a44cd86f3d8493f9d795ec9fcc6035 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Sep 2024 19:14:02 -0400 Subject: [PATCH] Implement SxS GraphQL users tests --- .../CreateSystemUserWithPermissionSet.graphql | 16 + .../CreateUserFromOAuthConnection.graphql | 14 + .../GQL/Mutations/CreateUserGroup.graphql | 40 + ...reateUserGroupWithInstanceListPerm.graphql | 40 + .../Mutations/CreateUserWithPassword.graphql | 14 + ...WithPasswordSelectOAuthConnections.graphql | 18 + .../GQL/Mutations/DeleteUserGroup.graphql | 11 + .../Mutations/SetFullPermsOnUserGroup.graphql | 69 ++ .../GQL/Mutations/SetUserGroup.graphql | 41 + .../Mutations/SetUserOAuthConnections.graphql | 25 + .../Mutations/SetUserPermissionSet.graphql | 66 ++ .../UpdateUserOAuthConnections.graphql | 18 + .../GQL/Queries/GetSomeGroupInfo.graphql | 41 + .../GQL/Queries/GetUserById.graphql | 14 + .../GQL/Queries/GetUserNameByNodeId.graphql | 8 + .../GQL/Queries/ListUserGroups.graphql | 14 + .../GQL/Queries/ListUsers.graphql | 12 + .../GQL/Queries/PageUserIds.graphql | 16 + .../GQL/Queries/ReadCurrentUser.graphql | 51 ++ .../Tgstation.Server.Tests/Live/ApiAssert.cs | 36 +- .../Live/GraphQLServerClientExtensions.cs | 47 + .../Live/IMultiServerClient.cs | 23 + .../Live/Instance/JobsHubTests.cs | 6 +- .../Live/MultiServerClient.cs | 13 +- .../Live/TestLiveServer.cs | 57 +- .../Tgstation.Server.Tests/Live/UsersTest.cs | 803 +++++++++++++----- 26 files changed, 1272 insertions(+), 241 deletions(-) create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroup.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/DeleteUserGroup.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserById.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserNameByNodeId.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUsers.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/PageUserIds.graphql create mode 100644 src/Tgstation.Server.Client.GraphQL/GQL/Queries/ReadCurrentUser.graphql create mode 100644 tests/Tgstation.Server.Tests/Live/GraphQLServerClientExtensions.cs create mode 100644 tests/Tgstation.Server.Tests/Live/IMultiServerClient.cs diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql new file mode 100644 index 0000000000..5c9e176dc4 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateSystemUserWithPermissionSet.graphql @@ -0,0 +1,16 @@ +mutation CreateSystemUserWithPermissionSet($systemIdentifier: String!) { + createUserBySystemIDAndPermissionSet( + input: { permissionSet: {}, systemIdentifier: $systemIdentifier } + ) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + user { + id + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql new file mode 100644 index 0000000000..9d374b5ff0 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserFromOAuthConnection.graphql @@ -0,0 +1,14 @@ +mutation CreateUserFromOAuthConnection($name: String!, $oAuthConnections: [OAuthConnectionInput!]!) { + createUserByOAuthAndPermissionSet(input: { name: $name, oAuthConnections: $oAuthConnections }) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + user { + id + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroup.graphql new file mode 100644 index 0000000000..2d8489ecb6 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroup.graphql @@ -0,0 +1,40 @@ +mutation CreateUserGroup($name: String!) { + createUserGroup(input: { name: $name }) { + userGroup { + id + name + permissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql new file mode 100644 index 0000000000..6a99c7ede0 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserGroupWithInstanceListPerm.graphql @@ -0,0 +1,40 @@ +mutation CreateUserGroupWithInstanceListPerm($name: String!) { + createUserGroup(input: { name: $name, permissionSet: { instanceManagerRights: { canList: true } } }) { + userGroup { + id + name + permissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql new file mode 100644 index 0000000000..8f330b0364 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPassword.graphql @@ -0,0 +1,14 @@ +mutation CreateUserWithPassword($name: String!, $password: String!) { + createUserByPasswordAndPermissionSet(input: { name: $name, password: $password }) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + user { + id + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql new file mode 100644 index 0000000000..2d6b335889 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/CreateUserWithPasswordSelectOAuthConnections.graphql @@ -0,0 +1,18 @@ +mutation CreateUserWithPasswordSelectOAuthConnections($name: String!, $password: String!) { + createUserByPasswordAndPermissionSet(input: { name: $name, password: $password }) { + user { + id + oAuthConnections { + externalUserId + provider + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/DeleteUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/DeleteUserGroup.graphql new file mode 100644 index 0000000000..0b94be4e05 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/DeleteUserGroup.graphql @@ -0,0 +1,11 @@ +mutation DeleteUserGroup($id: ID!) { + deleteEmptyUserGroup(input: { id: $id }) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql new file mode 100644 index 0000000000..1766cb4149 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetFullPermsOnUserGroup.graphql @@ -0,0 +1,69 @@ +mutation SetFullPermsOnUserGroup($id: ID!) { + updateUserGroup( + input: { + id: $id + newPermissionSet: { + administrationRights: { + canChangeVersion: true + canDownloadLogs: true + canEditOwnOAuthConnections: true + canEditOwnPassword: true + canReadUsers: true + canWriteUsers: true + canUploadVersion: true + canRestartHost: true + } + instanceManagerRights: { + canCreate: true + canDelete: true + canGrantPermissions: true + canSetOnline: true + canSetConfiguration: true + canSetChatBotLimit: true + canSetAutoUpdate: true + canRename: true + canRead: true + canList: true + canRelocate: true + } + } + } + ) { + userGroup { + id + name + permissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql new file mode 100644 index 0000000000..d68865e550 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserGroup.graphql @@ -0,0 +1,41 @@ +mutation SetUserGroup($id: ID!, $newGroupId: ID!) { + updateUserSetGroup(input: { id: $id, newGroupId: $newGroupId }) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + user { + ownedPermissionSet { + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + } + group { + id + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql new file mode 100644 index 0000000000..81746cf0b4 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserOAuthConnections.graphql @@ -0,0 +1,25 @@ +mutation SetUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectionInput!]!) { + updateUser( + input: { id: $id, newOAuthConnections: $newOAuthConnections } + ) { + user { + canonicalName + createdAt + enabled + id + name + systemIdentifier + oAuthConnections { + externalUserId + provider + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql new file mode 100644 index 0000000000..a1dbcb24e6 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/SetUserPermissionSet.graphql @@ -0,0 +1,66 @@ +mutation SetUserPermissionSet($id: ID!, $permissionSet: PermissionSetInput!) { + updateUserSetOwnedPermissionSet(input: { newPermissionSet: $permissionSet, id: $id }) { + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + user { + effectivePermissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + ownedPermissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + group { + id + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql new file mode 100644 index 0000000000..8cd18d2dc5 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/UpdateUserOAuthConnections.graphql @@ -0,0 +1,18 @@ +mutation UpdateUserOAuthConnections($id: ID!, $newOAuthConnections: [OAuthConnectionInput!]) { + updateUser(input: { id: $id, newOAuthConnections: $newOAuthConnections }) { + user { + id + oAuthConnections { + externalUserId + provider + } + } + errors { + ... on ErrorMessageError { + additionalData + errorCode + message + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql new file mode 100644 index 0000000000..f36dee754a --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetSomeGroupInfo.graphql @@ -0,0 +1,41 @@ +query GetSomeGroupInfo($id: ID!) { + swarm { + users { + groups { + byId(id: $id) { + permissionSet { + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + } + queryableUsersByGroup(first: 1) { + totalCount + nodes { + id + } + } + } + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserById.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserById.graphql new file mode 100644 index 0000000000..1dc6c3a8de --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserById.graphql @@ -0,0 +1,14 @@ +query GetUserById($id: ID!) { + swarm { + users { + byId(id: $id) { + canonicalName + createdAt + enabled + id + name + systemIdentifier + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserNameByNodeId.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserNameByNodeId.graphql new file mode 100644 index 0000000000..5565345e8a --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/GetUserNameByNodeId.graphql @@ -0,0 +1,8 @@ +query GetUserNameByNodeId($id: ID!) { + node(id: $id) { + ... on UserName { + id + name + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql new file mode 100644 index 0000000000..08bc93b4fd --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUserGroups.graphql @@ -0,0 +1,14 @@ +query ListUserGroups { + swarm { + users { + groups { + queryableGroups { + totalCount + nodes { + id + } + } + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUsers.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUsers.graphql new file mode 100644 index 0000000000..1553df9fc0 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ListUsers.graphql @@ -0,0 +1,12 @@ +query ListUsers { + swarm { + users { + queryableUsers { + nodes { + id + } + totalCount + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/PageUserIds.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/PageUserIds.graphql new file mode 100644 index 0000000000..8db5d7db4a --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/PageUserIds.graphql @@ -0,0 +1,16 @@ +query PageUserIds($first: Int, $after: String) { + swarm { + users { + queryableUsers(first: $first, after: $after) { + pageInfo { + endCursor + hasNextPage + } + totalCount + nodes { + id + } + } + } + } +} diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ReadCurrentUser.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ReadCurrentUser.graphql new file mode 100644 index 0000000000..dea2849d19 --- /dev/null +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Queries/ReadCurrentUser.graphql @@ -0,0 +1,51 @@ +query ReadCurrentUser { + swarm { + users { + current { + canonicalName + createdAt + enabled + id + name + systemIdentifier + group { + id + name + } + oAuthConnections { + externalUserId + provider + } + effectivePermissionSet { + administrationRights { + canChangeVersion + canDownloadLogs + canEditOwnOAuthConnections + canEditOwnPassword + canReadUsers + canRestartHost + canUploadVersion + canWriteUsers + } + instanceManagerRights { + canCreate + canDelete + canGrantPermissions + canList + canRead + canRelocate + canRename + canSetAutoUpdate + canSetChatBotLimit + canSetConfiguration + canSetOnline + } + } + createdBy { + id + name + } + } + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/ApiAssert.cs b/tests/Tgstation.Server.Tests/Live/ApiAssert.cs index 1fe6742015..5bd736c6cc 100644 --- a/tests/Tgstation.Server.Tests/Live/ApiAssert.cs +++ b/tests/Tgstation.Server.Tests/Live/ApiAssert.cs @@ -1,9 +1,15 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Api.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using StrawberryShake; + using Tgstation.Server.Client; +using Tgstation.Server.Client.GraphQL; namespace Tgstation.Server.Tests.Live { @@ -19,7 +25,7 @@ namespace Tgstation.Server.Tests.Live /// A resulting in a . /// The expected . /// A representing the running operation, - public static async ValueTask ThrowsException(Func action, ErrorCode? expectedErrorCode = null) + public static async ValueTask ThrowsException(Func action, Api.Models.ErrorCode? expectedErrorCode = null) where TApiException : ApiException { try @@ -43,7 +49,7 @@ namespace Tgstation.Server.Tests.Live /// A resulting in a . /// The expected . /// A representing the running operation, - public static async ValueTask ThrowsException(Func> action, ErrorCode? expectedErrorCode = null) + public static async ValueTask ThrowsException(Func> action, Api.Models.ErrorCode? expectedErrorCode = null) where TApiException : ApiException { try @@ -58,5 +64,25 @@ namespace Tgstation.Server.Tests.Live Assert.Fail($"Expected exception {typeof(TApiException)}!"); } + + public static async ValueTask OperationFails( + IGraphQLServerClient client, + Func>> operationInvoker, + Func payloadSelector, + Client.GraphQL.ErrorCode expectedErrorCode, + CancellationToken cancellationToken) + where TResultData : class + { + var operationResult = await client.RunOperation(operationInvoker, cancellationToken); + operationResult.EnsureNoErrors(); + + var payload = payloadSelector(operationResult.Data); + + var payloadErrors = (IEnumerable)payload.GetType().GetProperty("Errors").GetValue(payload); + var error = payloadErrors.Single(); + + var errorCode = (ErrorCode)error.GetType().GetProperty("ErrorCode").GetValue(error); + Assert.AreEqual(expectedErrorCode, errorCode); + } } } diff --git a/tests/Tgstation.Server.Tests/Live/GraphQLServerClientExtensions.cs b/tests/Tgstation.Server.Tests/Live/GraphQLServerClientExtensions.cs new file mode 100644 index 0000000000..993b8999ad --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/GraphQLServerClientExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using StrawberryShake; + +using Tgstation.Server.Client.GraphQL; + +namespace Tgstation.Server.Tests.Live +{ + static class GraphQLServerClientExtensions + { + public static async ValueTask RunQueryEnsureNoErrors( + this IGraphQLServerClient serverClient, + Func>> operationExecutor, + CancellationToken cancellationToken) + where TResultData : class + { + var result = await serverClient.RunOperation(operationExecutor, cancellationToken); + result.EnsureNoErrors(); + return result.Data; + } + + public static async ValueTask RunMutationEnsureNoErrors( + this IGraphQLServerClient serverClient, + Func>> operationExecutor, + Func payloadSelector, + CancellationToken cancellationToken) + where TResultData : class + { + var result = await serverClient.RunOperation(operationExecutor, cancellationToken); + result.EnsureNoErrors(); + var data = payloadSelector(result.Data); + var errorsObject = data.GetType().GetProperty("Errors").GetValue(data); + if (errorsObject != null) + { + var errorsCount = (int)errorsObject.GetType().GetProperty("Count").GetValue(errorsObject); + + Assert.AreEqual(0, errorsCount); + } + + return data; + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/IMultiServerClient.cs b/tests/Tgstation.Server.Tests/Live/IMultiServerClient.cs new file mode 100644 index 0000000000..d617c63288 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/IMultiServerClient.cs @@ -0,0 +1,23 @@ +using StrawberryShake; +using System.Threading.Tasks; +using System.Threading; +using System; +using Tgstation.Server.Client.GraphQL; +using Tgstation.Server.Client; + +namespace Tgstation.Server.Tests.Live +{ + interface IMultiServerClient + { + ValueTask Execute( + Func restAction, + Func graphQLAction); + + ValueTask<(TRestResult, TGraphQLResult)> ExecuteReadOnlyConfirmEquivalence( + Func> restAction, + Func>> graphQLAction, + Func comparison, + CancellationToken cancellationToken) + where TGraphQLResult : class; + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs index dc8c71753d..f2bb84c639 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs @@ -34,10 +34,10 @@ namespace Tgstation.Server.Tests.Live.Instance long? permlessPsId; - public JobsHubTests(IRestServerClient permedUser, IRestServerClient permlessUser) + public JobsHubTests(MultiServerClient permedUser, MultiServerClient permlessUser) { - this.permedUser = permedUser; - this.permlessUser = permlessUser; + this.permedUser = permedUser.RestClient; + this.permlessUser = permlessUser.RestClient; Assert.AreNotSame(permedUser, permlessUser); diff --git a/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs b/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs index 270be6f8f7..451ae08c47 100644 --- a/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs +++ b/tests/Tgstation.Server.Tests/Live/MultiServerClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -12,7 +12,7 @@ using Tgstation.Server.Common.Extensions; namespace Tgstation.Server.Tests.Live { - sealed class MultiServerClient : IAsyncDisposable + sealed class MultiServerClient : IMultiServerClient, IAsyncDisposable { public IRestServerClient RestClient { get; } public IGraphQLServerClient GraphQLClient { get; } @@ -40,7 +40,7 @@ namespace Tgstation.Server.Tests.Live return restAction(RestClient); } - public async ValueTask ExecuteReadOnlyConfirmEquivalence( + public async ValueTask<(TRestResult, TGraphQLResult)> ExecuteReadOnlyConfirmEquivalence( Func> restAction, Func>> graphQLAction, Func comparison, @@ -50,8 +50,13 @@ namespace Tgstation.Server.Tests.Live var restTask = restAction(RestClient); var graphQLResult = await GraphQLClient.RunOperation(graphQLAction, cancellationToken); + graphQLResult.EnsureNoErrors(); + var restResult = await restTask; - Assert.IsTrue(comparison(restResult, graphQLResult.Data), "REST/GraphQL results differ!"); + var comparisonResult = comparison(restResult, graphQLResult.Data); + Assert.IsTrue(comparisonResult, "REST/GraphQL results differ!"); + + return (restResult, graphQLResult.Data); } } } diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 66f8817a4c..93776764e6 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -1379,20 +1379,25 @@ namespace Tgstation.Server.Tests.Live await unAuthedMultiClient.ExecuteReadOnlyConfirmEquivalence( restClient => restClient.ServerInformation(cancellationToken), gqlClient => gqlClient.UnauthenticatedServerInformation.ExecuteAsync(cancellationToken), - (restServerInfo, gqlServerInfo) => restServerInfo.ApiVersion.Major == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.MajorApiVersion - && (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos - || restServerInfo.OAuthProviderInfos.All(kvp => - { - var info = gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos.FirstOrDefault(x => (int)x.Key == (int)kvp.Key); - return info != null - && info.Value.ServerUrl == kvp.Value.ServerUrl - && info.Value.ClientId == kvp.Value.ClientId - && info.Value.RedirectUri == kvp.Value.RedirectUri; - })), + (restServerInfo, gqlServerInfo) => + { + var result = restServerInfo.ApiVersion.Major == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.MajorApiVersion + && (restServerInfo.OAuthProviderInfos == gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos + || restServerInfo.OAuthProviderInfos.All(kvp => + { + var info = gqlServerInfo.Swarm.CurrentNode.Gateway.Information.OAuthProviderInfos.FirstOrDefault(x => (int)x.Key == (int)kvp.Key); + return info != null + && info.Value.ServerUrl == kvp.Value.ServerUrl + && info.Value.ClientId == kvp.Value.ClientId + && info.Value.RedirectUri == kvp.Value.RedirectUri; + })); + + return result; + }, cancellationToken); } - async ValueTask CreateUserWithNoInstancePerms() + async ValueTask CreateUserWithNoInstancePerms() { var createRequest = new UserCreateRequest() { @@ -1408,10 +1413,10 @@ namespace Tgstation.Server.Tests.Live var user = await firstAdminRestClient.Users.Create(createRequest, cancellationToken); Assert.IsTrue(user.Enabled); - return await restClientFactory.CreateFromLogin(server.RootUrl, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken); + return await CreateClient(server.RootUrl, createRequest.Name, createRequest.Password, false, cancellationToken); } - var jobsHubTest = new JobsHubTests(firstAdminRestClient, await CreateUserWithNoInstancePerms()); + var jobsHubTest = new JobsHubTests(firstAdminMultiClient, await CreateUserWithNoInstancePerms()); Task jobsHubTestTask; { if (server.DumpOpenApiSpecpath) @@ -1449,7 +1454,7 @@ namespace Tgstation.Server.Tests.Live jobsHubTestTask = FailFast(await jobsHubTest.Run(cancellationToken)); // returns Task var rootTest = FailFast(RawRequestTests.Run(restClientFactory, firstAdminRestClient, cancellationToken)); var adminTest = FailFast(new AdministrationTest(firstAdminRestClient.Administration).Run(cancellationToken)); - var usersTest = FailFast(new UsersTest(firstAdminRestClient).Run(cancellationToken)); + var usersTest = FailFast(new UsersTest(firstAdminMultiClient).Run(cancellationToken).AsTask()); var instanceManagerTest = new InstanceManagerTest(firstAdminRestClient, server.Directory); var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken); @@ -1836,7 +1841,15 @@ namespace Tgstation.Server.Tests.Live await serverTask; } - async ValueTask CreateAdminClient(Uri url, CancellationToken cancellationToken) + ValueTask CreateAdminClient(Uri url, CancellationToken cancellationToken) + => CreateClient(url, DefaultCredentials.AdminUserName, DefaultCredentials.DefaultAdminUserPassword, true, cancellationToken); + + async ValueTask CreateClient( + Uri url, + string username, + string password, + bool retry, + CancellationToken cancellationToken = default) { url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty)); var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); @@ -1850,14 +1863,14 @@ namespace Tgstation.Server.Tests.Live restClientTask = restClientFactory.CreateFromLogin( url, - DefaultCredentials.AdminUserName, - DefaultCredentials.DefaultAdminUserPassword, + username, + password, cancellationToken: cancellationToken); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); graphQLClientTask = graphQLClientFactory.CreateFromLogin( url, - DefaultCredentials.AdminUserName, - DefaultCredentials.DefaultAdminUserPassword, + username, + password, cancellationToken: cts.Token); IRestServerClient restClient; @@ -1898,14 +1911,14 @@ namespace Tgstation.Server.Tests.Live catch (HttpRequestException) { //migrating, to be expected - if (DateTimeOffset.UtcNow > giveUpAt) + if (DateTimeOffset.UtcNow > giveUpAt || !retry) throw; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } catch (ServiceUnavailableException) { // migrating, to be expected - if (DateTimeOffset.UtcNow > giveUpAt) + if (DateTimeOffset.UtcNow > giveUpAt || !retry) throw; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } diff --git a/tests/Tgstation.Server.Tests/Live/UsersTest.cs b/tests/Tgstation.Server.Tests/Live/UsersTest.cs index ceebac4356..3336490681 100644 --- a/tests/Tgstation.Server.Tests/Live/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/Live/UsersTest.cs @@ -1,5 +1,11 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Elastic.CommonSchema; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using StrawberryShake; + using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -10,23 +16,25 @@ using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; +using Tgstation.Server.Client.GraphQL; using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests.Live { sealed class UsersTest { - readonly IRestServerClient serverClient; + readonly IMultiServerClient serverClient; - public UsersTest(IRestServerClient serverClient) + public UsersTest(IMultiServerClient serverClient) { this.serverClient = serverClient ?? throw new ArgumentNullException(nameof(serverClient)); } - public async Task Run(CancellationToken cancellationToken) + public async ValueTask Run(CancellationToken cancellationToken) { - await Task.WhenAll( + await ValueTaskExtensions.WhenAll( BasicTests(cancellationToken), TestCreateSysUser(cancellationToken), TestSpamCreation(cancellationToken)); @@ -34,214 +42,525 @@ namespace Tgstation.Server.Tests.Live await TestPagination(cancellationToken); } - async Task BasicTests(CancellationToken cancellationToken) + async ValueTask BasicTests(CancellationToken cancellationToken) { - var user = await serverClient.Users.Read(cancellationToken); - Assert.IsNotNull(user); - Assert.AreEqual("Admin", user.Name); - Assert.IsNull(user.SystemIdentifier); - Assert.AreEqual(true, user.Enabled); - Assert.IsNotNull(user.OAuthConnections); - Assert.IsNotNull(user.PermissionSet); - Assert.IsNotNull(user.PermissionSet.Id); - Assert.IsNotNull(user.PermissionSet.InstanceManagerRights); - Assert.IsNotNull(user.PermissionSet.AdministrationRights); + var (restUser, gqlUser) = await serverClient.ExecuteReadOnlyConfirmEquivalence( + client => client.Users.Read(cancellationToken), + client => client.ReadCurrentUser.ExecuteAsync(cancellationToken), + (restResult, graphQLResult) => + { + var gqlUser = graphQLResult.Swarm.Users.Current; + return restResult.Enabled == gqlUser.Enabled + && restResult.Name == gqlUser.Name + && (restResult.CreatedAt.Value.Ticks / 1000000) == (gqlUser.CreatedAt.Ticks / 1000000) + && restResult.SystemIdentifier == gqlUser.SystemIdentifier + && restResult.CreatedBy.Name == gqlUser.CreatedBy.Name; + }, + cancellationToken); - var systemUser = user.CreatedBy; + Assert.IsNotNull(restUser); + Assert.AreEqual("Admin", restUser.Name); + Assert.IsNull(restUser.SystemIdentifier); + Assert.AreEqual(true, restUser.Enabled); + Assert.IsNotNull(restUser.OAuthConnections); + Assert.IsNotNull(restUser.PermissionSet); + Assert.IsNotNull(restUser.PermissionSet.Id); + Assert.IsNotNull(restUser.PermissionSet.InstanceManagerRights); + Assert.IsNotNull(restUser.PermissionSet.AdministrationRights); + + var systemUser = restUser.CreatedBy; Assert.IsNotNull(systemUser); Assert.AreEqual("TGS", systemUser.Name); - var users = await serverClient.Users.List(null, cancellationToken); - Assert.IsTrue(users.Count > 0); - Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); - - await ApiAssert.ThrowsException(() => serverClient.Users.GetId(systemUser, cancellationToken)); - await ApiAssert.ThrowsException(() => serverClient.Users.Update(new UserUpdateRequest - { - Id = systemUser.Id - }, cancellationToken)); - - var sampleOAuthConnections = new List - { - new OAuthConnection + await serverClient.Execute( + async client => { - ExternalUserId = "asdfasdf", - Provider = OAuthProvider.Discord - } - }; - await ApiAssert.ThrowsException(() => serverClient.Users.Update(new UserUpdateRequest - { - Id = user.Id, - OAuthConnections = sampleOAuthConnections - }, cancellationToken), ErrorCode.AdminUserCannotOAuth); + var users = await client.Users.List(null, cancellationToken); + Assert.IsTrue(users.Count > 0); + Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); - var testUser = await serverClient.Users.Create( - new UserCreateRequest - { - Name = $"BasicTestUser", - Password = "asdfasdjfhauwiehruiy273894234jhndjkwh" + await ApiAssert.ThrowsException(() => client.Users.GetId(systemUser, cancellationToken)); + await ApiAssert.ThrowsException(() => client.Users.Update(new UserUpdateRequest + { + Id = systemUser.Id + }, cancellationToken)); + + var sampleOAuthConnections = new List + { + new() + { + ExternalUserId = "asdfasdf", + Provider = Api.Models.OAuthProvider.Discord + } + }; + + await ApiAssert.ThrowsException(() => client.Users.Update(new UserUpdateRequest + { + Id = restUser.Id, + OAuthConnections = sampleOAuthConnections + }, cancellationToken), Api.Models.ErrorCode.AdminUserCannotOAuth); + + var testUser = await client.Users.Create( + new UserCreateRequest + { + Name = $"BasicTestUser", + Password = "asdfasdjfhauwiehruiy273894234jhndjkwh" + }, + cancellationToken); + + Assert.IsNotNull(testUser.OAuthConnections); + testUser = await client.Users.Update( + new UserUpdateRequest + { + Id = testUser.Id, + OAuthConnections = sampleOAuthConnections + }, + cancellationToken); + + Assert.AreEqual(1, testUser.OAuthConnections.Count); + Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections.First().ExternalUserId); + Assert.AreEqual(sampleOAuthConnections.First().Provider, testUser.OAuthConnections.First().Provider); + + var group = await client.Groups.Create( + new UserGroupCreateRequest + { + Name = "TestGroup" + }, + cancellationToken); + Assert.AreEqual(group.Name, "TestGroup"); + Assert.IsNotNull(group.PermissionSet); + Assert.IsNotNull(group.PermissionSet.Id); + Assert.AreEqual(AdministrationRights.None, group.PermissionSet.AdministrationRights); + Assert.AreEqual(InstanceManagerRights.None, group.PermissionSet.InstanceManagerRights); + + var group2 = await client.Groups.Create(new UserGroupCreateRequest + { + Name = "TestGroup2", + PermissionSet = new PermissionSet + { + InstanceManagerRights = InstanceManagerRights.List + } + }, cancellationToken); + Assert.AreEqual(AdministrationRights.None, group2.PermissionSet.AdministrationRights); + Assert.AreEqual(InstanceManagerRights.List, group2.PermissionSet.InstanceManagerRights); + + var groups = await client.Groups.List(null, cancellationToken); + Assert.AreEqual(2, groups.Count); + + foreach (var igroup in groups) + { + Assert.IsNotNull(igroup.Users); + Assert.IsNotNull(igroup.PermissionSet); + } + + await client.Groups.Delete(group2, cancellationToken); + + groups = await client.Groups.List(null, cancellationToken); + Assert.AreEqual(1, groups.Count); + + group = await client.Groups.Update(new UserGroupUpdateRequest + { + Id = groups[0].Id, + PermissionSet = new PermissionSet + { + InstanceManagerRights = RightsHelper.AllRights(), + AdministrationRights = RightsHelper.AllRights(), + } + }, cancellationToken); + + Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.AdministrationRights); + Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.InstanceManagerRights); + + UserUpdateRequest testUserUpdate = new UserCreateRequest + { + Name = "TestUserWithNoPassword", + Password = string.Empty + }; + + await ApiAssert.ThrowsException(() => client.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken), Api.Models.ErrorCode.UserPasswordLength); + + testUserUpdate.OAuthConnections = + [ + new() + { + ExternalUserId = "asdf", + Provider = Api.Models.OAuthProvider.GitHub + } + ]; + + var testUser2 = await client.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken); + + testUserUpdate = new UserUpdateRequest + { + Id = testUser2.Id, + PermissionSet = testUser2.PermissionSet, + Group = new Api.Models.Internal.UserGroup + { + Id = group.Id + }, + }; + await ApiAssert.ThrowsException( + () => client.Users.Update( + testUserUpdate, + cancellationToken), + Api.Models.ErrorCode.UserGroupAndPermissionSet); + + testUserUpdate.PermissionSet = null; + + testUser2 = await client.Users.Update(testUserUpdate, cancellationToken); + + Assert.IsNull(testUser2.PermissionSet); + Assert.IsNotNull(testUser2.Group); + Assert.AreEqual(group.Id, testUser2.Group.Id); + + var group4 = await client.Groups.GetId(group, cancellationToken); + Assert.IsNotNull(group4.Users); + Assert.AreEqual(1, group4.Users.Count); + Assert.AreEqual(testUser2.Id, group4.Users.First().Id); + Assert.IsNotNull(group4.PermissionSet); + + testUserUpdate.Group = null; + testUserUpdate.PermissionSet = new PermissionSet + { + AdministrationRights = RightsHelper.AllRights(), + InstanceManagerRights = RightsHelper.AllRights(), + }; + + testUser2 = await client.Users.Update(testUserUpdate, cancellationToken); + Assert.IsNull(testUser2.Group); + Assert.IsNotNull(testUser2.PermissionSet); }, - cancellationToken); - - Assert.IsNotNull(testUser.OAuthConnections); - testUser = await serverClient.Users.Update( - new UserUpdateRequest - { - Id = testUser.Id, - OAuthConnections = sampleOAuthConnections - }, - cancellationToken); - - Assert.AreEqual(1, testUser.OAuthConnections.Count); - Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections.First().ExternalUserId); - Assert.AreEqual(sampleOAuthConnections.First().Provider, testUser.OAuthConnections.First().Provider); - - - var group = await serverClient.Groups.Create( - new UserGroupCreateRequest + async client => { - Name = "TestGroup" - }, - cancellationToken); - Assert.AreEqual(group.Name, "TestGroup"); - Assert.IsNotNull(group.PermissionSet); - Assert.IsNotNull(group.PermissionSet.Id); - Assert.AreEqual(AdministrationRights.None, group.PermissionSet.AdministrationRights); - Assert.AreEqual(InstanceManagerRights.None, group.PermissionSet.InstanceManagerRights); + var result = await client.RunOperation(gql => gql.ListUsers.ExecuteAsync(cancellationToken), cancellationToken); + result.EnsureNoErrors(); + var users = result.Data.Swarm.Users.QueryableUsers; + Assert.IsTrue(users.TotalCount > 0); + Assert.AreEqual(Math.Min(ApiController.DefaultPageSize, users.TotalCount), users.Nodes.Count); - var group2 = await serverClient.Groups.Create(new UserGroupCreateRequest - { - Name = "TestGroup2", - PermissionSet = new PermissionSet - { - InstanceManagerRights = InstanceManagerRights.List - } - }, cancellationToken); - Assert.AreEqual(AdministrationRights.None, group2.PermissionSet.AdministrationRights); - Assert.AreEqual(InstanceManagerRights.List, group2.PermissionSet.InstanceManagerRights); + var tgsUserResult = await client.RunOperation(gql => gql.GetUserNameByNodeId.ExecuteAsync(gqlUser.Swarm.Users.Current.CreatedBy.Id, cancellationToken), cancellationToken); + tgsUserResult.EnsureNoErrors(); + var tgsUserResult2 = await client.RunOperation(gql => gql.GetUserById.ExecuteAsync(gqlUser.Swarm.Users.Current.CreatedBy.Id, cancellationToken), cancellationToken); + Assert.IsTrue(tgsUserResult2.IsErrorResult()); - var groups = await serverClient.Groups.List(null, cancellationToken); - Assert.AreEqual(2, groups.Count); + var sampleOAuthConnections = new List + { + new() + { + ExternalUserId = "asdfasdf", + Provider = Client.GraphQL.OAuthProvider.Discord, + } + }; - foreach (var igroup in groups) - { - Assert.IsNotNull(igroup.Users); - Assert.IsNotNull(igroup.PermissionSet); - } + await ApiAssert.OperationFails( + client, + gql => gql.SetUserOAuthConnections.ExecuteAsync( + gqlUser.Swarm.Users.Current.Id, + sampleOAuthConnections, + cancellationToken), + data => data.UpdateUser, + Client.GraphQL.ErrorCode.AdminUserCannotOAuth, + cancellationToken); - await serverClient.Groups.Delete(group2, cancellationToken); + var testUserResult = await client.RunMutationEnsureNoErrors( + gql => gql.CreateUserWithPasswordSelectOAuthConnections.ExecuteAsync("BasicTestUser", "asdfasdjfhauwiehruiy273894234jhndjkwh", cancellationToken), + data => data.CreateUserByPasswordAndPermissionSet, + cancellationToken); - groups = await serverClient.Groups.List(null, cancellationToken); - Assert.AreEqual(1, groups.Count); + var testUserResult2 = await client.RunMutationEnsureNoErrors( + gql => gql.UpdateUserOAuthConnections.ExecuteAsync( + testUserResult.User.Id, + sampleOAuthConnections, + cancellationToken), + data => data.UpdateUser, + cancellationToken); - group = await serverClient.Groups.Update(new UserGroupUpdateRequest - { - Id = groups[0].Id, - PermissionSet = new PermissionSet - { - InstanceManagerRights = RightsHelper.AllRights(), - AdministrationRights = RightsHelper.AllRights(), - } - }, cancellationToken); + var testUser = testUserResult2.User; + Assert.IsNotNull(testUser.OAuthConnections); + Assert.AreEqual(1, testUser.OAuthConnections.Count); + Assert.AreEqual(sampleOAuthConnections.First().ExternalUserId, testUser.OAuthConnections[0].ExternalUserId); + Assert.AreEqual(sampleOAuthConnections.First().Provider, testUser.OAuthConnections[0].Provider); - Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.AdministrationRights); - Assert.AreEqual(RightsHelper.AllRights(), group.PermissionSet.InstanceManagerRights); + var groupResult = await client.RunMutationEnsureNoErrors( + gql => gql.CreateUserGroup.ExecuteAsync("TestGroup", cancellationToken), + data => data.CreateUserGroup, + cancellationToken); - UserUpdateRequest testUserUpdate = new UserCreateRequest - { - Name = "TestUserWithNoPassword", - Password = string.Empty - }; + var group = groupResult.UserGroup; + Assert.AreEqual(group.Name, "TestGroup"); + Assert.IsNotNull(group.PermissionSet); - await ApiAssert.ThrowsException(() => serverClient.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken), ErrorCode.UserPasswordLength); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanSetOnline); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanRelocate); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanRename); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanSetConfiguration); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanRead); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanCreate); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanDelete); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanGrantPermissions); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanList); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanSetAutoUpdate); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanSetChatBotLimit); + Assert.IsFalse(group.PermissionSet.InstanceManagerRights.CanSetConfiguration); - testUserUpdate.OAuthConnections = new List - { - new OAuthConnection - { - ExternalUserId = "asdf", - Provider = OAuthProvider.GitHub - } - }; + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanChangeVersion); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanDownloadLogs); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanEditOwnPassword); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanReadUsers); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanRestartHost); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanUploadVersion); + Assert.IsFalse(group.PermissionSet.AdministrationRights.CanWriteUsers); - var testUser2 = await serverClient.Users.Create((UserCreateRequest)testUserUpdate, cancellationToken); + var group2Result = await client.RunMutationEnsureNoErrors( + gql => gql.CreateUserGroupWithInstanceListPerm.ExecuteAsync("TestGroup2", cancellationToken), + data => data.CreateUserGroup, + cancellationToken); - testUserUpdate = new UserUpdateRequest - { - Id = testUser2.Id, - PermissionSet = testUser2.PermissionSet, - Group = new Api.Models.Internal.UserGroup - { - Id = group.Id - }, - }; - await ApiAssert.ThrowsException( - () => serverClient.Users.Update( - testUserUpdate, - cancellationToken), - ErrorCode.UserGroupAndPermissionSet); + var group2 = group2Result.UserGroup; - testUserUpdate.PermissionSet = null; + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanSetOnline); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanRelocate); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanRename); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanSetConfiguration); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanRead); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanCreate); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanDelete); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanGrantPermissions); + Assert.IsTrue(group2.PermissionSet.InstanceManagerRights.CanList); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanSetAutoUpdate); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanSetChatBotLimit); + Assert.IsFalse(group2.PermissionSet.InstanceManagerRights.CanSetConfiguration); - testUser2 = await serverClient.Users.Update(testUserUpdate, cancellationToken); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanChangeVersion); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanDownloadLogs); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanEditOwnPassword); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanReadUsers); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanRestartHost); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanUploadVersion); + Assert.IsFalse(group2.PermissionSet.AdministrationRights.CanWriteUsers); - Assert.IsNull(testUser2.PermissionSet); - Assert.IsNotNull(testUser2.Group); - Assert.AreEqual(group.Id, testUser2.Group.Id); + var groupsResult = await client.RunQueryEnsureNoErrors( + gql => gql.ListUserGroups.ExecuteAsync(cancellationToken), + cancellationToken); - group = await serverClient.Groups.GetId(group, cancellationToken); - Assert.IsNotNull(group.Users); - Assert.AreEqual(1, group.Users.Count); - Assert.AreEqual(testUser2.Id, group.Users.First().Id); - Assert.IsNotNull(group.PermissionSet); + var groups = groupsResult.Swarm.Users.Groups.QueryableGroups; + Assert.AreEqual(2, groups.TotalCount); - testUserUpdate.Group = null; - testUserUpdate.PermissionSet = new PermissionSet - { - AdministrationRights = RightsHelper.AllRights(), - InstanceManagerRights = RightsHelper.AllRights(), - }; + foreach (var igroup in groups.Nodes) + Assert.IsNotNull(igroup.Id); - testUser2 = await serverClient.Users.Update(testUserUpdate, cancellationToken); - Assert.IsNull(testUser2.Group); - Assert.IsNotNull(testUser2.PermissionSet); + var deleteResult = await client.RunMutationEnsureNoErrors( + gql => gql.DeleteUserGroup.ExecuteAsync(group2.Id, cancellationToken), + data => data.DeleteEmptyUserGroup, + cancellationToken); + + groupsResult = await client.RunQueryEnsureNoErrors( + gql => gql.ListUserGroups.ExecuteAsync(cancellationToken), + cancellationToken); + + groups = groupsResult.Swarm.Users.Groups.QueryableGroups; + Assert.AreEqual(1, groups.TotalCount); + + foreach (var igroup in groups.Nodes) + Assert.IsNotNull(igroup.Id); + + var group3Result = await client.RunMutationEnsureNoErrors( + gql => gql.SetFullPermsOnUserGroup.ExecuteAsync(group.Id, cancellationToken), + data => data.UpdateUserGroup, + cancellationToken); + + var group3 = group3Result.UserGroup; + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanSetOnline); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanRelocate); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanRename); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanSetConfiguration); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanRead); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanCreate); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanDelete); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanGrantPermissions); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanList); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanSetAutoUpdate); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanSetChatBotLimit); + Assert.IsTrue(group3.PermissionSet.InstanceManagerRights.CanSetConfiguration); + + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanChangeVersion); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanDownloadLogs); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanEditOwnOAuthConnections); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanEditOwnPassword); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanReadUsers); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanRestartHost); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanUploadVersion); + Assert.IsTrue(group3.PermissionSet.AdministrationRights.CanWriteUsers); + + await ApiAssert.OperationFails( + client, + gql => gql.CreateUserWithPassword.ExecuteAsync("TestUserWithNoPassword", String.Empty, cancellationToken), + data => data.CreateUserByPasswordAndPermissionSet, + Client.GraphQL.ErrorCode.ModelValidationFailure, + cancellationToken); + + await ApiAssert.OperationFails( + client, + gql => gql.CreateUserWithPassword.ExecuteAsync("TestUserWithShortPassword", "a", cancellationToken), + data => data.CreateUserByPasswordAndPermissionSet, + Client.GraphQL.ErrorCode.UserPasswordLength, + cancellationToken); + + var oAuthCreateResult = await client.RunMutationEnsureNoErrors( + gql => gql.CreateUserFromOAuthConnection.ExecuteAsync( + "TestUserWithNoPassword", + [ + new() + { + ExternalUserId = "asdf", + Provider = Client.GraphQL.OAuthProvider.GitHub, + } + ], + cancellationToken), + data => data.CreateUserByOAuthAndPermissionSet, + cancellationToken); + + var testUser2 = oAuthCreateResult.User; + + var testUser22Result = await client.RunMutationEnsureNoErrors( + gql => gql.SetUserGroup.ExecuteAsync(testUser2.Id, group.Id, cancellationToken), + data => data.UpdateUserSetGroup, + cancellationToken); + + var testUser22 = testUser22Result.User; + + Assert.IsNull(testUser22.OwnedPermissionSet); + Assert.IsNotNull(testUser22.Group); + Assert.AreEqual(group.Id, testUser22.Group.Id); + + var group4Result = await client.RunQueryEnsureNoErrors( + gql => gql.GetSomeGroupInfo.ExecuteAsync(group.Id, cancellationToken), + cancellationToken); + var group4 = group4Result.Swarm.Users.Groups.ById; + + Assert.IsNotNull(group4.QueryableUsersByGroup.Nodes); + Assert.AreEqual(1, group4.QueryableUsersByGroup.TotalCount); + Assert.AreEqual(testUser2.Id, group4.QueryableUsersByGroup.Nodes[0].Id); + Assert.IsNotNull(group4.PermissionSet); + + var testUser4Result = await client.RunMutationEnsureNoErrors( + gql => gql.SetUserPermissionSet.ExecuteAsync( + testUser2.Id, + new PermissionSetInput + { + AdministrationRights = new AdministrationRightsFlagsInput + { + CanChangeVersion = true, + CanDownloadLogs = true, + CanEditOwnOAuthConnections = true, + CanEditOwnPassword = true, + CanReadUsers = true, + CanRestartHost = true, + CanUploadVersion = true, + CanWriteUsers = true, + }, + InstanceManagerRights = new InstanceManagerRightsFlagsInput + { + CanCreate = true, + CanDelete = true, + CanGrantPermissions = true, + CanList = true, + CanRead = true, + CanRelocate = true, + CanRename = true, + CanSetAutoUpdate = true, + CanSetChatBotLimit = true, + CanSetConfiguration = true, + CanSetOnline = true, + } + }, + cancellationToken), + data => data.UpdateUserSetOwnedPermissionSet, + cancellationToken); + + var testUser4 = testUser4Result.User; + Assert.IsNull(testUser4.Group); + Assert.IsNotNull(testUser4.OwnedPermissionSet); + }); } - async Task TestCreateSysUser(CancellationToken cancellationToken) + ValueTask TestCreateSysUser(CancellationToken cancellationToken) { var sysId = Environment.UserName; - var update = new UserCreateRequest - { - SystemIdentifier = sysId - }; - if (new PlatformIdentifier().IsWindows) - await serverClient.Users.Create(update, cancellationToken); - else - await ApiAssert.ThrowsException(() => serverClient.Users.Create(update, cancellationToken), ErrorCode.RequiresPosixSystemIdentity); + + return serverClient.Execute( + async restClient => + { + var update = new UserCreateRequest + { + SystemIdentifier = sysId + }; + if (new PlatformIdentifier().IsWindows) + await restClient.Users.Create(update, cancellationToken); + else + await ApiAssert.ThrowsException(() => restClient.Users.Create(update, cancellationToken), Api.Models.ErrorCode.RequiresPosixSystemIdentity); + }, + async graphQLClient => + { + if (new PlatformIdentifier().IsWindows) + { + await graphQLClient.RunMutationEnsureNoErrors( + gql => gql.CreateSystemUserWithPermissionSet.ExecuteAsync(sysId, cancellationToken), + data => data.CreateUserBySystemIDAndPermissionSet, + cancellationToken); + } + else + await ApiAssert.OperationFails( + graphQLClient, + gql => gql.CreateSystemUserWithPermissionSet.ExecuteAsync(sysId, cancellationToken), + data => data.CreateUserBySystemIDAndPermissionSet, + Client.GraphQL.ErrorCode.RequiresPosixSystemIdentity, + cancellationToken); + }); } - async Task TestSpamCreation(CancellationToken cancellationToken) + async ValueTask TestSpamCreation(CancellationToken cancellationToken) { // Careful with this, very easy to overload the thread pool const int RepeatCount = 100; - var tasks = new List>(RepeatCount); + var tasks = new List(RepeatCount); ThreadPool.GetMaxThreads(out var defaultMaxWorker, out var defaultMaxCompletion); ThreadPool.GetMinThreads(out var defaultMinWorker, out var defaultMinCompletion); + ConcurrentBag ids = new ConcurrentBag(); try { ThreadPool.SetMinThreads(Math.Min(RepeatCount * 4, defaultMaxWorker), Math.Min(RepeatCount * 4, defaultMaxCompletion)); for (int i = 0; i < RepeatCount; ++i) { - var task = - serverClient.Users.Create( - new UserCreateRequest + var iLocal = i; + ValueTask CreateSpamUser() + => serverClient.Execute( + async restClient => { - Name = $"SpamTestUser_{i}", - Password = "asdfasdjfhauwiehruiy273894234jhndjkwh" + var user = await restClient.Users.Create( + new UserCreateRequest + { + Name = $"SpamTestUser_{iLocal}", + Password = "asdfasdjfhauwiehruiy273894234jhndjkwh" + }, + cancellationToken); + + ids.Add(user.Id); }, - cancellationToken); - tasks.Add(task); + async graphQLClient => + { + var result = await graphQLClient.RunMutationEnsureNoErrors( + gql => gql.CreateUserWithPassword.ExecuteAsync($"SpamTestUser_{iLocal}", "asdfasdjfhauwiehruiy273894234jhndjkwh", cancellationToken), + data => data.CreateUserByPasswordAndPermissionSet, + cancellationToken); + + ids.Add(result.User.Id); + }); + + tasks.Add(CreateSpamUser()); } await ValueTaskExtensions.WhenAll(tasks); @@ -251,57 +570,127 @@ namespace Tgstation.Server.Tests.Live ThreadPool.SetMinThreads(defaultMinWorker, defaultMinCompletion); } - Assert.AreEqual(RepeatCount, tasks.Select(task => task.Result.Id).Distinct().Count(), "Did not receive expected number of unique user IDs!"); + Assert.AreEqual(RepeatCount, ids.Distinct().Count(), "Did not receive expected number of unique user IDs!"); } - async Task TestPagination(CancellationToken cancellationToken) + ValueTask TestPagination(CancellationToken cancellationToken) { - // we test pagination here b/c it's the only spot we have a decent amount of entities - var nullSettings = await serverClient.Users.List(null, cancellationToken); - var emptySettings = await serverClient.Users.List( - new PaginationSettings + const int ExpectedCount = 106; + return serverClient.Execute( + async restClient => { - }, cancellationToken); + // we test pagination here b/c it's the only spot we have a decent amount of entities + var nullSettings = await restClient.Users.List(null, cancellationToken); + var emptySettings = await restClient.Users.List( + new PaginationSettings + { + }, cancellationToken); - Assert.AreEqual(nullSettings.Count, emptySettings.Count); - Assert.IsTrue(nullSettings.All(x => emptySettings.SingleOrDefault(y => x.Id == y.Id) != null)); + Assert.AreEqual(nullSettings.Count, emptySettings.Count); + Assert.AreEqual(ExpectedCount, nullSettings.Count); + Assert.IsTrue(nullSettings.All(x => emptySettings.SingleOrDefault(y => x.Id == y.Id) != null)); - await ApiAssert.ThrowsException>(() => serverClient.Users.List( - new PaginationSettings - { - PageSize = -2143 - }, cancellationToken), ErrorCode.ApiInvalidPageOrPageSize); - await ApiAssert.ThrowsException>(() => serverClient.Users.List( - new PaginationSettings - { - PageSize = int.MaxValue - }, cancellationToken), ErrorCode.ApiPageTooLarge); + await ApiAssert.ThrowsException>(() => restClient.Users.List( + new PaginationSettings + { + PageSize = -2143 + }, cancellationToken), Api.Models.ErrorCode.ApiInvalidPageOrPageSize); + await ApiAssert.ThrowsException>(() => restClient.Users.List( + new PaginationSettings + { + PageSize = ApiController.MaximumPageSize + 1, + }, cancellationToken), Api.Models.ErrorCode.ApiPageTooLarge); - await serverClient.Users.List( - new PaginationSettings - { - PageSize = 50 + var users = await restClient.Users.List( + new PaginationSettings + { + PageSize = ApiController.MaximumPageSize, + RetrieveCount = ApiController.MaximumPageSize, + }, + cancellationToken); + + Assert.AreEqual(ApiController.MaximumPageSize, users.Count); + + var skipped = await restClient.Users.List(new PaginationSettings + { + Offset = 50, + RetrieveCount = 5 + }, cancellationToken); + Assert.AreEqual(5, skipped.Count); + + var allAfterSkipped = await restClient.Users.List(new PaginationSettings + { + Offset = 50, + }, cancellationToken); + Assert.IsTrue(5 < allAfterSkipped.Count); + + var limited = await restClient.Users.List(new PaginationSettings + { + RetrieveCount = 12, + }, cancellationToken); + Assert.AreEqual(12, limited.Count); }, - cancellationToken); + graphQLClient => + { + async ValueTask TestPageSize(int? inputPageSize) + { + var outputPageSize = inputPageSize ?? ApiController.DefaultPageSize; - var skipped = await serverClient.Users.List(new PaginationSettings - { - Offset = 50, - RetrieveCount = 5 - }, cancellationToken); - Assert.AreEqual(5, skipped.Count); + string cursor = null; - var allAfterSkipped = await serverClient.Users.List(new PaginationSettings - { - Offset = 50, - }, cancellationToken); - Assert.IsTrue(5 < allAfterSkipped.Count); + var exactMatch = (ExpectedCount % outputPageSize) == 0; + var expectedIterations = (ExpectedCount / outputPageSize) + (exactMatch ? 0 : 1); + for (int i = 0; i < expectedIterations; ++i) + { + var isLastIteration = i == expectedIterations - 1; + var queryable = (await graphQLClient.RunQueryEnsureNoErrors( + gql => gql.PageUserIds.ExecuteAsync(inputPageSize, cursor, cancellationToken), + cancellationToken)) + .Swarm + .Users + .QueryableUsers; - var limited = await serverClient.Users.List(new PaginationSettings - { - RetrieveCount = 12, - }, cancellationToken); - Assert.AreEqual(12, limited.Count); + if (!isLastIteration || exactMatch) + Assert.AreEqual(outputPageSize, queryable.Nodes.Count); + else + Assert.AreEqual(ExpectedCount % outputPageSize, queryable.Nodes.Count); + Assert.AreEqual(!isLastIteration, queryable.PageInfo.HasNextPage); + Assert.IsNotNull(queryable.PageInfo.EndCursor); + + cursor = queryable.PageInfo.EndCursor; + } + } + + async ValueTask TestBadPageSize(int size) + { + var result = await graphQLClient.RunOperation( + gql => gql.PageUserIds.ExecuteAsync(size, null, cancellationToken), + cancellationToken); + + if (size == 0) + { + // special case + result.EnsureNoErrors(); + Assert.AreEqual(0, result.Data.Swarm.Users.QueryableUsers.Nodes.Count); + return; + } + + var errored = result.IsErrorResult(); + Assert.IsTrue(errored); + } + + return ValueTaskExtensions.WhenAll( + TestPageSize(null), + TestPageSize(ApiController.DefaultPageSize), + TestPageSize(ApiController.DefaultPageSize / 2), + TestPageSize(ApiController.MaximumPageSize), + TestPageSize(1), + TestBadPageSize(-1), + TestBadPageSize(0), + TestBadPageSize(ApiController.MaximumPageSize + 1), + TestBadPageSize(Int32.MinValue), + TestBadPageSize(Int32.MaxValue)); + }); } } }