Merge pull request #9597 from owncloud/listInvitationTest
[test-only] list Invitation
This commit is contained in:
@@ -561,7 +561,7 @@ OCM_OCM_INVITE_MANAGER_INSECURE=true \
|
|||||||
OCM_OCM_SHARE_PROVIDER_INSECURE=true \
|
OCM_OCM_SHARE_PROVIDER_INSECURE=true \
|
||||||
OCM_OCM_STORAGE_PROVIDER_INSECURE=true \
|
OCM_OCM_STORAGE_PROVIDER_INSECURE=true \
|
||||||
OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE="${workspaceFolder}/tests/config/drone/providers.json" \
|
OCM_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE="${workspaceFolder}/tests/config/drone/providers.json" \
|
||||||
OCIS_ADD_RUN_SERVICES="ocm"
|
OCIS_ADD_RUN_SERVICES="ocm" \
|
||||||
ocis/bin/ocis server
|
ocis/bin/ocis server
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ To enable ocm in the web interface, you need to set the following envs:
|
|||||||
`FRONTEND_OCS_INCLUDE_OCM_SHAREES=true` \
|
`FRONTEND_OCS_INCLUDE_OCM_SHAREES=true` \
|
||||||
`FRONTEND_OCS_LIST_OCM_SHARES=true` \
|
`FRONTEND_OCS_LIST_OCM_SHARES=true` \
|
||||||
`FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true` \
|
`FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING=true` \
|
||||||
`FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true`
|
`FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING=true` \
|
||||||
|
|
||||||
and put `ocm` to apps https://github.com/owncloud/ocis/blob/master/services/web/pkg/config/defaults/defaultconfig.go#L101
|
and put `ocm` to apps https://github.com/owncloud/ocis/blob/master/services/web/pkg/config/defaults/defaultconfig.go#L101
|
||||||
{{< /hint>}}
|
{{< /hint>}}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
namespace TestHelpers;
|
namespace TestHelpers;
|
||||||
|
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Psr\Http\Message\RequestInterface;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,4 +131,29 @@ class OcmHelper {
|
|||||||
self::getRequestHeaders()
|
self::getRequestHeaders()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $baseUrl
|
||||||
|
* @param string $xRequestId
|
||||||
|
* @param string $user
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public static function listInvite(
|
||||||
|
string $baseUrl,
|
||||||
|
string $xRequestId,
|
||||||
|
string $user,
|
||||||
|
string $password
|
||||||
|
): ResponseInterface {
|
||||||
|
$url = self::getFullUrl($baseUrl, 'list-invite');
|
||||||
|
return HttpRequestHelper::get(
|
||||||
|
$url,
|
||||||
|
$xRequestId,
|
||||||
|
$user,
|
||||||
|
$password,
|
||||||
|
self::getRequestHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ default:
|
|||||||
- OcmContext:
|
- OcmContext:
|
||||||
- SharingNgContext:
|
- SharingNgContext:
|
||||||
- SpacesContext:
|
- SpacesContext:
|
||||||
|
- OcisConfigContext:
|
||||||
|
|
||||||
cliCommands:
|
cliCommands:
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -312,5 +312,8 @@ The expected failures in this file are from features in the owncloud/ocis repo.
|
|||||||
- [apiOcm/share.feature:12](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiOcm/share.feature#L12)
|
- [apiOcm/share.feature:12](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiOcm/share.feature#L12)
|
||||||
- [apiOcm/share.feature:91](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiOcm/share.feature#L91)
|
- [apiOcm/share.feature:91](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiOcm/share.feature#L91)
|
||||||
|
|
||||||
|
### [OCM. user cannot see invite description and inviteUser email](https://github.com/owncloud/ocis/issues/9591)
|
||||||
|
|
||||||
|
- [apiOcm/createInvitation.feature:63](https://github.com/owncloud/ocis/blob/master/tests/acceptance/features/apiOcm/createInvitation.feature#L63)
|
||||||
- Note: always have an empty line at the end of this file.
|
- Note: always have an empty line at the end of this file.
|
||||||
The bash script that processes this file requires that the last line has a newline on the end.
|
The bash script that processes this file requires that the last line has a newline on the end.
|
||||||
|
|||||||
@@ -14,28 +14,7 @@ Feature: accepting invitation
|
|||||||
|
|
||||||
Scenario: user accepts invitation
|
Scenario: user accepts invitation
|
||||||
Given using server "LOCAL"
|
Given using server "LOCAL"
|
||||||
When "Alice" creates the federation share invitation
|
And "Alice" has created the federation share invitation
|
||||||
Then the HTTP status code should be "200"
|
|
||||||
And the JSON data of the response should match
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"expiration",
|
|
||||||
"token"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"expiration": {
|
|
||||||
"type": "integer",
|
|
||||||
"pattern": "^[0-9]{10}$"
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%fed_invitation_token_pattern%$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
When using server "REMOTE"
|
When using server "REMOTE"
|
||||||
And "Brian" accepts the last federation share invitation
|
And "Brian" accepts the last federation share invitation
|
||||||
Then the HTTP status code should be "200"
|
Then the HTTP status code should be "200"
|
||||||
@@ -43,28 +22,7 @@ Feature: accepting invitation
|
|||||||
|
|
||||||
Scenario: user accepts invitation sent with email and description
|
Scenario: user accepts invitation sent with email and description
|
||||||
Given using server "LOCAL"
|
Given using server "LOCAL"
|
||||||
When "Alice" creates the federation share invitation with email "alice@example.com" and description "a share invitation from Alice"
|
And "Alice" has created the federation share invitation with email "brian@example.com" and description "a share invitation from Alice"
|
||||||
Then the HTTP status code should be "200"
|
|
||||||
And the JSON data of the response should match
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"expiration",
|
|
||||||
"token"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"expiration": {
|
|
||||||
"type": "integer",
|
|
||||||
"pattern": "^[0-9]{10}$"
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^%fed_invitation_token_pattern%$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
When using server "REMOTE"
|
When using server "REMOTE"
|
||||||
And "Brian" accepts the last federation share invitation
|
And "Brian" accepts the last federation share invitation
|
||||||
Then the HTTP status code should be "200"
|
Then the HTTP status code should be "200"
|
||||||
@@ -135,3 +93,56 @@ Feature: accepting invitation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@env-config
|
||||||
|
Scenario: user cannot accept expired invitation tokens
|
||||||
|
Given using server "LOCAL"
|
||||||
|
And the config "OCM_OCM_INVITE_MANAGER_TOKEN_EXPIRATION" has been set to "1s"
|
||||||
|
And "Alice" has created the federation share invitation
|
||||||
|
When using server "REMOTE"
|
||||||
|
And the user waits "2" seconds for the token to expire
|
||||||
|
And "Brian" tries to accept the last federation share invitation
|
||||||
|
Then the HTTP status code should be "400"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"const": "INVALID_PARAMETER"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"const": "token has expired"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
Scenario: user cannot accept invalid invitation token
|
||||||
|
Given using server "LOCAL"
|
||||||
|
And "Alice" tries to accept the invitation with invalid token
|
||||||
|
Then the HTTP status code should be "404"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"code",
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"const": "RESOURCE_NOT_FOUND"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"const": "token not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
@ocm
|
||||||
|
Feature: create invitation
|
||||||
|
As a user
|
||||||
|
I can create an invitations and send it to the person I want to share with
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given user "Alice" has been created with default attributes and without skeleton files
|
||||||
|
|
||||||
|
|
||||||
|
Scenario: user creates invitation
|
||||||
|
Given using server "LOCAL"
|
||||||
|
When "Alice" creates the federation share invitation
|
||||||
|
Then the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"expiration",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": "integer",
|
||||||
|
"pattern": "^[0-9]{10}$"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^%fed_invitation_token_pattern%$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
When "Alice" lists the created invitations
|
||||||
|
Then the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"expiration",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": "integer",
|
||||||
|
"pattern": "^[0-9]{10}$"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^%fed_invitation_token_pattern%$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@issue-9591
|
||||||
|
Scenario: user creates invitation with email and description
|
||||||
|
Given using server "LOCAL"
|
||||||
|
When "Alice" creates the federation share invitation with email "brian@example.com" and description "a share invitation from Alice"
|
||||||
|
Then the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"expiration",
|
||||||
|
"token",
|
||||||
|
"description",
|
||||||
|
"recipient"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": "integer",
|
||||||
|
"pattern": "^[0-9]{10}$"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^%fed_invitation_token_pattern%$"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "a share invitation from Alice"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "brian@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
And the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"expiration",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": "integer",
|
||||||
|
"pattern": "^[0-9]{10}$"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^%fed_invitation_token_pattern%$"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "a share invitation from Alice"
|
||||||
|
},
|
||||||
|
"recipient": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "brian@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@env-config
|
||||||
|
Scenario: user cannot see expired invitation tokens
|
||||||
|
Given using server "LOCAL"
|
||||||
|
And the config "OCM_OCM_INVITE_MANAGER_TOKEN_EXPIRATION" has been set to "1s"
|
||||||
|
When "Alice" creates the federation share invitation
|
||||||
|
Then the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"expiration",
|
||||||
|
"token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"expiration": {
|
||||||
|
"type": "integer",
|
||||||
|
"pattern": "^[0-9]{10}$"
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^%fed_invitation_token_pattern%$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
And the user waits "2" seconds for the token to expire
|
||||||
|
When "Alice" lists the created invitations
|
||||||
|
Then the HTTP status code should be "200"
|
||||||
|
And the JSON data of the response should match
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 0,
|
||||||
|
"maxItems": 0
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -8,7 +8,7 @@ Feature: an user shares resources usin ScienceMesh application
|
|||||||
And using server "REMOTE"
|
And using server "REMOTE"
|
||||||
And user "Brian" has been created with default attributes and without skeleton files
|
And user "Brian" has been created with default attributes and without skeleton files
|
||||||
|
|
||||||
|
@issue-9534
|
||||||
Scenario: users shares folder to federation users after receiver accepted invitation
|
Scenario: users shares folder to federation users after receiver accepted invitation
|
||||||
Given using server "LOCAL"
|
Given using server "LOCAL"
|
||||||
And "Alice" has created the federation share invitation
|
And "Alice" has created the federation share invitation
|
||||||
@@ -87,7 +87,7 @@ Feature: an user shares resources usin ScienceMesh application
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@issue-9534
|
||||||
Scenario: users shares folder to federation users after accepting invitation
|
Scenario: users shares folder to federation users after accepting invitation
|
||||||
Given using server "LOCAL"
|
Given using server "LOCAL"
|
||||||
And "Alice" has created the federation share invitation
|
And "Alice" has created the federation share invitation
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use GuzzleHttp\Exception\GuzzleException;
|
|||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use TestHelpers\OcisHelper;
|
use TestHelpers\OcisHelper;
|
||||||
use TestHelpers\OcmHelper;
|
use TestHelpers\OcmHelper;
|
||||||
|
use TestHelpers\WebDavHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acceptance test steps related to testing federation share(ocm) features
|
* Acceptance test steps related to testing federation share(ocm) features
|
||||||
@@ -122,31 +123,35 @@ class OcmContext implements Context {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given :user has created the federation share invitation
|
* @Given :user has created the federation share invitation
|
||||||
|
* @Given :user has created the federation share invitation with email :email and description :description
|
||||||
*
|
*
|
||||||
* @param string $user
|
* @param string $user
|
||||||
|
* @param string $email
|
||||||
|
* @param string $description
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @throws GuzzleException
|
* @throws GuzzleException
|
||||||
*/
|
*/
|
||||||
public function userHasCreatedTheFederationShareInvitation(string $user): void {
|
public function userHasCreatedTheFederationShareInvitation(string $user, $email = null, $description = null): void {
|
||||||
$response = $this->createInvitation($user);
|
$response = $this->createInvitation($user, $email, $description);
|
||||||
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
|
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $user
|
* @param string $user
|
||||||
|
* @param string $token
|
||||||
*
|
*
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
* @throws GuzzleException
|
* @throws GuzzleException
|
||||||
*/
|
*/
|
||||||
public function acceptInvitation(string $user): ResponseInterface {
|
public function acceptInvitation(string $user, string $token = null): ResponseInterface {
|
||||||
$providerDomain = ($this->featureContext->getCurrentServer() === "LOCAL") ? $this->getFedOcisDomain() : $this->getOcisDomain();
|
$providerDomain = ($this->featureContext->getCurrentServer() === "LOCAL") ? $this->getFedOcisDomain() : $this->getOcisDomain();
|
||||||
return OcmHelper::acceptInvitation(
|
return OcmHelper::acceptInvitation(
|
||||||
$this->featureContext->getBaseUrl(),
|
$this->featureContext->getBaseUrl(),
|
||||||
$this->featureContext->getStepLineRef(),
|
$this->featureContext->getStepLineRef(),
|
||||||
$user,
|
$user,
|
||||||
$this->featureContext->getPasswordForUser($user),
|
$this->featureContext->getPasswordForUser($user),
|
||||||
$this->invitationToken,
|
$token ? $token : $this->invitationToken,
|
||||||
$providerDomain
|
$providerDomain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -164,6 +169,18 @@ class OcmContext implements Context {
|
|||||||
$this->featureContext->setResponse($this->acceptInvitation($user));
|
$this->featureContext->setResponse($this->acceptInvitation($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When :user tries to accept the invitation with invalid token
|
||||||
|
*
|
||||||
|
* @param string $user
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function userTriesToAcceptInvitationWithInvalidToken(string $user): void {
|
||||||
|
$this->featureContext->setResponse($this->acceptInvitation($user, WebDavHelper::generateUUIDv4()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Given :user has accepted invitation
|
* @Given :user has accepted invitation
|
||||||
*
|
*
|
||||||
@@ -203,4 +220,43 @@ class OcmContext implements Context {
|
|||||||
public function userFindsAcceptedUsers(string $user): void {
|
public function userFindsAcceptedUsers(string $user): void {
|
||||||
$this->featureContext->setResponse($this->findAcceptedUsers($user));
|
$this->featureContext->setResponse($this->findAcceptedUsers($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $user
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function listInvitations(string $user): ResponseInterface {
|
||||||
|
return OcmHelper::listInvite(
|
||||||
|
$this->featureContext->getBaseUrl(),
|
||||||
|
$this->featureContext->getStepLineRef(),
|
||||||
|
$user,
|
||||||
|
$this->featureContext->getPasswordForUser($user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When :user lists the created invitations
|
||||||
|
*
|
||||||
|
* @param string $user
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function userListsCreatedInvitations(string $user): void {
|
||||||
|
$this->featureContext->setResponse($this->listInvitations($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When the user waits :number seconds for the token to expire
|
||||||
|
*
|
||||||
|
* @param int $number
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws GuzzleException
|
||||||
|
*/
|
||||||
|
public function theUserWaitsForTokenToExpire(int $number): void {
|
||||||
|
\sleep($number);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user