复制项目
This commit is contained in:
136
test/e2e/README.md
Normal file
136
test/e2e/README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# OpenIM End-to-End (E2E) Testing Module
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains the End-to-End (E2E) testing suite for OpenIM, a comprehensive instant messaging platform. The E2E tests are designed to simulate real-world usage scenarios to ensure that all components of the OpenIM system are functioning correctly in an integrated environment.
|
||||
|
||||
The tests cover various aspects of the system, including API endpoints, chat services, web interfaces, and RPC components, as well as performance and scalability under different load conditions.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```bash
|
||||
❯ tree e2e
|
||||
test/e2e/
|
||||
├── conformance/ # Contains tests for verifying OpenIM API conformance
|
||||
├── framework/ # Provides auxiliary code and libraries for building and running E2E tests
|
||||
│ ├── config/ # Test configuration files and management
|
||||
│ ├── ginkgowrapper/ # Functions wrapping the testing library for handling test failures and skips
|
||||
│ └── helpers/ # Helper functions such as user creation, message sending, etc.
|
||||
├── api/ # End-to-end tests for OpenIM API
|
||||
├── chat/ # Tests for the business server (including login, registration, and other logic)
|
||||
├── web/ # Tests for the web frontend (login, registration, message sending and receiving)
|
||||
├── rpc/ # End-to-end tests for various RPC components
|
||||
│ ├── auth/ # Tests for the authentication service
|
||||
│ ├── conversation/ # Tests for conversation management
|
||||
│ ├── friend/ # Tests for friend relationship management
|
||||
│ ├── group/ # Tests for group management
|
||||
│ └── message/ # Tests for message handling
|
||||
├── scalability/ # Tests for the scalability of the OpenIM system
|
||||
├── performance/ # Performance tests such as load testing and stress testing
|
||||
└── upgrade/ # Tests for compatibility and stability during OpenIM upgrades
|
||||
```
|
||||
|
||||
The E2E tests are organized into the following directory structure:
|
||||
|
||||
- `conformance/`: Contains tests to verify the conformance of OpenIM API implementations.
|
||||
- `framework/`: Provides helper code for constructing and running E2E tests using the Ginkgo framework.
|
||||
- `config/`: Manages test configurations and options.
|
||||
- `ginkgowrapper/`: Wrappers for Ginkgo's `Fail` and `Skip` functions to handle structured data panics.
|
||||
- `helpers/`: Utility functions for common test actions like user creation, message dispatching, etc.
|
||||
- `api/`: E2E tests for the OpenIM API endpoints.
|
||||
- `chat/`: Tests for the chat service, including authentication, session management, and messaging logic.
|
||||
- `web/`: Tests for the web interface, including user interactions and information exchange.
|
||||
- `rpc/`: E2E tests for each of the RPC components.
|
||||
- `auth/`: Tests for the authentication service.
|
||||
- `conversation/`: Tests for conversation management.
|
||||
- `friend/`: Tests for friend relationship management.
|
||||
- `group/`: Tests for group management.
|
||||
- `message/`: Tests for message handling.
|
||||
- `scalability/`: Tests for the scalability of the OpenIM system.
|
||||
- `performance/`: Performance tests, including load and stress tests.
|
||||
- `upgrade/`: Tests for the upgrade process of OpenIM, ensuring compatibility and stability.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Since the deployment of OpenIM requires some components such as Mongo and Kafka, you should think a bit before using E2E tests
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
OR User [kubernetes deployment](https://github.com/openimsdk/helm-charts)
|
||||
|
||||
Before running the E2E tests, ensure that you have the following prerequisites installed:
|
||||
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- Ginkgo test framework
|
||||
- Go (version 1.19 or higher)
|
||||
|
||||
## Configuration
|
||||
|
||||
Test configurations can be customized via the `config/` directory. The configuration files are in YAML format and allow you to set parameters such as API endpoints, user credentials, and test data.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run a single test or set of tests, you'll need the [Ginkgo](https://github.com/onsi/ginkgo) tool installed on your machine:
|
||||
|
||||
```
|
||||
ginkgo --help
|
||||
--focus value
|
||||
If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.
|
||||
```
|
||||
|
||||
To run the entire suite of E2E tests, use the following command:
|
||||
|
||||
```sh
|
||||
ginkgo -v --randomizeAllSpecs --randomizeSuites --failOnPending --cover --trace --race --progress
|
||||
```
|
||||
|
||||
You can also run a specific test or group of tests by specifying the path to the test directory:
|
||||
|
||||
```bash
|
||||
ginkgo -v ./test/e2e/chat
|
||||
```
|
||||
|
||||
Or you can use Makefile to run the tests:
|
||||
|
||||
```bash
|
||||
make test-e2e
|
||||
```
|
||||
|
||||
## Test Development
|
||||
|
||||
To contribute to the E2E tests:
|
||||
|
||||
1. Clone the repository and navigate to the `test/e2e/` directory.
|
||||
2. Create a new test file or modify an existing test to cover a new scenario.
|
||||
3. Write test cases using the Ginkgo BDD style, ensuring that they are clear and descriptive.
|
||||
4. Run the tests locally to ensure they pass.
|
||||
5. Submit a pull request with your changes.
|
||||
|
||||
Please refer to the `CONTRIBUTING.md` file for more detailed instructions on contributing to the test suite.
|
||||
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you encounter any issues while running the E2E tests, please open an issue on the GitHub repository with the following information:
|
||||
|
||||
Open issue: https://github.com/openimsdk/open-im-server-deploy/issues/new/choose, choose "Failing Test" template.
|
||||
|
||||
+ A clear and concise description of the issue.
|
||||
+ Steps to reproduce the behavior.
|
||||
+ Relevant logs and test output.
|
||||
+ Any other context that could be helpful in troubleshooting.
|
||||
|
||||
|
||||
## Continuous Integration (CI)
|
||||
|
||||
The E2E test suite is integrated with CI, which runs the tests automatically on each code commit. The results are reported back to the pull request or commit to provide immediate feedback on the impact of the changes.
|
||||
|
||||
[](https://github.com/openimsdk/open-im-server-deploy/actions/workflows/e2e-test.yml)
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
For any queries or assistance, please reach out to the OpenIM development team at [support@openim.com](mailto:support@openim.com).
|
||||
149
test/e2e/api/token/token.go
Normal file
149
test/e2e/api/token/token.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// API endpoints and other constants.
|
||||
const (
|
||||
APIHost = "http://127.0.0.1:10002"
|
||||
UserTokenURL = APIHost + "/auth/user_token"
|
||||
UserRegisterURL = APIHost + "/user/user_register"
|
||||
SecretKey = "openIM123"
|
||||
OperationID = "1646445464564"
|
||||
)
|
||||
|
||||
// UserTokenRequest represents a request to get a user token.
|
||||
type UserTokenRequest struct {
|
||||
Secret string `json:"secret"`
|
||||
PlatformID int `json:"platformID"`
|
||||
UserID string `json:"userID"`
|
||||
}
|
||||
|
||||
// UserTokenResponse represents a response containing a user token.
|
||||
type UserTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
ErrCode int `json:"errCode"`
|
||||
}
|
||||
|
||||
// User represents user data for registration.
|
||||
type User struct {
|
||||
UserID string `json:"userID"`
|
||||
Nickname string `json:"nickname"`
|
||||
FaceURL string `json:"faceURL"`
|
||||
}
|
||||
|
||||
// UserRegisterRequest represents a request to register a user.
|
||||
type UserRegisterRequest struct {
|
||||
Users []User `json:"users"`
|
||||
}
|
||||
|
||||
/* func main() {
|
||||
// Example usage of functions
|
||||
token, err := GetUserToken("openIM123456")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting user token: %v", err)
|
||||
}
|
||||
fmt.Println("Token:", token)
|
||||
|
||||
err = RegisterUser(token, "testUserID", "TestNickname", "https://example.com/image.jpg")
|
||||
if err != nil {
|
||||
log.Fatalf("Error registering user: %v", err)
|
||||
}
|
||||
} */
|
||||
|
||||
// GetUserToken requests a user token from the API.
|
||||
func GetUserToken(userID string) (string, error) {
|
||||
reqBody := UserTokenRequest{
|
||||
Secret: SecretKey,
|
||||
PlatformID: 1,
|
||||
UserID: userID,
|
||||
}
|
||||
reqBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Post(UserTokenURL, "application/json", bytes.NewBuffer(reqBytes))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var tokenResp UserTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tokenResp.ErrCode != 0 {
|
||||
return "", fmt.Errorf("error in token response: %v", tokenResp.ErrCode)
|
||||
}
|
||||
|
||||
return tokenResp.Token, nil
|
||||
}
|
||||
|
||||
// RegisterUser registers a new user using the API.
|
||||
func RegisterUser(token, userID, nickname, faceURL string) error {
|
||||
user := User{
|
||||
UserID: userID,
|
||||
Nickname: nickname,
|
||||
FaceURL: faceURL,
|
||||
}
|
||||
reqBody := UserRegisterRequest{
|
||||
Users: []User{user},
|
||||
}
|
||||
reqBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", UserRegisterURL, bytes.NewBuffer(reqBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("operationID", OperationID)
|
||||
req.Header.Add("token", token)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var respData map[string]any
|
||||
if err := json.Unmarshal(respBody, &respData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if errCode, ok := respData["errCode"].(float64); ok && errCode != 0 {
|
||||
return fmt.Errorf("error in user registration response: %v", respData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
71
test/e2e/api/user/curd.go
Normal file
71
test/e2e/api/user/curd.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gettoken "git.imall.cloud/openim/open-im-server-deploy/test/e2e/api/token"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/test/e2e/framework/config"
|
||||
)
|
||||
|
||||
// UserInfoRequest represents a request to get or update user information.
|
||||
type UserInfoRequest struct {
|
||||
UserIDs []string `json:"userIDs,omitempty"`
|
||||
UserInfo *gettoken.User `json:"userInfo,omitempty"`
|
||||
}
|
||||
|
||||
// GetUsersOnlineStatusRequest represents a request to get users' online status.
|
||||
type GetUsersOnlineStatusRequest struct {
|
||||
UserIDs []string `json:"userIDs"`
|
||||
}
|
||||
|
||||
// GetUsersInfo retrieves detailed information for a list of user IDs.
|
||||
func GetUsersInfo(token string, userIDs []string) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/user/get_users_info", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := UserInfoRequest{
|
||||
UserIDs: userIDs,
|
||||
}
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
|
||||
// UpdateUserInfo updates the information for a user.
|
||||
func UpdateUserInfo(token, userID, nickname, faceURL string) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/user/update_user_info", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := UserInfoRequest{
|
||||
UserInfo: &gettoken.User{
|
||||
UserID: userID,
|
||||
Nickname: nickname,
|
||||
FaceURL: faceURL,
|
||||
},
|
||||
}
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
|
||||
// GetUsersOnlineStatus retrieves the online status for a list of user IDs.
|
||||
func GetUsersOnlineStatus(token string, userIDs []string) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/user/get_users_online_status", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := GetUsersOnlineStatusRequest{
|
||||
UserIDs: userIDs,
|
||||
}
|
||||
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
125
test/e2e/api/user/user.go
Normal file
125
test/e2e/api/user/user.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
gettoken "git.imall.cloud/openim/open-im-server-deploy/test/e2e/api/token"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/test/e2e/framework/config"
|
||||
)
|
||||
|
||||
// ForceLogoutRequest represents a request to force a user logout.
|
||||
type ForceLogoutRequest struct {
|
||||
PlatformID int `json:"platformID"`
|
||||
UserID string `json:"userID"`
|
||||
}
|
||||
|
||||
// CheckUserAccountRequest represents a request to check a user account.
|
||||
type CheckUserAccountRequest struct {
|
||||
CheckUserIDs []string `json:"checkUserIDs"`
|
||||
}
|
||||
|
||||
// GetUsersRequest represents a request to get a list of users.
|
||||
type GetUsersRequest struct {
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
// Pagination specifies the page number and number of items per page.
|
||||
type Pagination struct {
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ShowNumber int `json:"showNumber"`
|
||||
}
|
||||
|
||||
// ForceLogout forces a user to log out.
|
||||
func ForceLogout(token, userID string, platformID int) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/auth/force_logout", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := ForceLogoutRequest{
|
||||
PlatformID: platformID,
|
||||
UserID: userID,
|
||||
}
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
|
||||
// CheckUserAccount checks if the user accounts exist.
|
||||
func CheckUserAccount(token string, userIDs []string) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/user/account_check", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := CheckUserAccountRequest{
|
||||
CheckUserIDs: userIDs,
|
||||
}
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
|
||||
// GetUsers retrieves a list of users with pagination.
|
||||
func GetUsers(token string, pageNumber, showNumber int) error {
|
||||
|
||||
url := fmt.Sprintf("http://%s:%s/user/account_check", config.LoadConfig().APIHost, config.LoadConfig().APIPort)
|
||||
|
||||
requestBody := GetUsersRequest{
|
||||
Pagination: Pagination{
|
||||
PageNumber: pageNumber,
|
||||
ShowNumber: showNumber,
|
||||
},
|
||||
}
|
||||
return sendPostRequestWithToken(url, token, requestBody)
|
||||
}
|
||||
|
||||
// sendPostRequestWithToken sends a POST request with a token in the header.
|
||||
func sendPostRequestWithToken(url, token string, body any) error {
|
||||
reqBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("operationID", gettoken.OperationID)
|
||||
req.Header.Add("token", token)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var respData map[string]any
|
||||
if err := json.Unmarshal(respBody, &respData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if errCode, ok := respData["errCode"].(float64); ok && errCode != 0 {
|
||||
return fmt.Errorf("error in response: %v", respData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1
test/e2e/conformance/.keep
Normal file
1
test/e2e/conformance/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
51
test/e2e/e2e.go
Normal file
51
test/e2e/e2e.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gettoken "git.imall.cloud/openim/open-im-server-deploy/test/e2e/api/token"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/test/e2e/api/user"
|
||||
)
|
||||
|
||||
// RunE2ETests checks configuration parameters (specified through flags) and then runs
|
||||
// E2E tests using the Ginkgo runner.
|
||||
// If a "report directory" is specified, one or more JUnit test reports will be
|
||||
// generated in this directory, and cluster logs will also be saved.
|
||||
// This function is called on each Ginkgo node in parallel mode.
|
||||
func RunE2ETests(t *testing.T) {
|
||||
|
||||
// Example usage of new functions
|
||||
token, _ := gettoken.GetUserToken("openIM123456")
|
||||
|
||||
// Example of getting user info
|
||||
_ = user.GetUsersInfo(token, []string{"user1", "user2"})
|
||||
|
||||
// Example of updating user info
|
||||
_ = user.UpdateUserInfo(token, "user1", "NewNickname", "https://github.com/openimsdk/open-im-server-deploy/blob/main/assets/logo/openim-logo.png")
|
||||
|
||||
// Example of getting users' online status
|
||||
_ = user.GetUsersOnlineStatus(token, []string{"user1", "user2"})
|
||||
|
||||
// Example of forcing a logout
|
||||
_ = user.ForceLogout(token, "4950983283", 2)
|
||||
|
||||
// Example of checking user account
|
||||
_ = user.CheckUserAccount(token, []string{"openIM123456", "anotherUserID"})
|
||||
|
||||
// Example of getting users
|
||||
_ = user.GetUsers(token, 1, 100)
|
||||
}
|
||||
37
test/e2e/e2e_test.go
Normal file
37
test/e2e/e2e_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/test/e2e/framework/config"
|
||||
)
|
||||
|
||||
// handleFlags sets up all flags and parses the command line.
|
||||
func handleFlags() {
|
||||
config.CopyFlags(config.Flags, flag.CommandLine)
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
handleFlags()
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestE2E(t *testing.T) {
|
||||
RunE2ETests(t)
|
||||
}
|
||||
84
test/e2e/framework/config/config.go
Normal file
84
test/e2e/framework/config/config.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags is the flag set that AddOptions adds to. Test authors should
|
||||
// also use it instead of directly adding to the global command line.
|
||||
var Flags = flag.NewFlagSet("", flag.ContinueOnError)
|
||||
|
||||
// CopyFlags ensures that all flags that are defined in the source flag
|
||||
// set appear in the target flag set as if they had been defined there
|
||||
// directly. From the flag package it inherits the behavior that there
|
||||
// is a panic if the target already contains a flag from the source.
|
||||
func CopyFlags(source *flag.FlagSet, target *flag.FlagSet) {
|
||||
source.VisitAll(func(flag *flag.Flag) {
|
||||
// We don't need to copy flag.DefValue. The original
|
||||
// default (from, say, flag.String) was stored in
|
||||
// the value and gets extracted by Var for the help
|
||||
// message.
|
||||
target.Var(flag.Value, flag.Name, flag.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
// Config defines the configuration structure for the OpenIM components.
|
||||
type Config struct {
|
||||
APIHost string
|
||||
APIPort string
|
||||
MsgGatewayHost string
|
||||
MsgTransferHost string
|
||||
PushHost string
|
||||
RPCAuthHost string
|
||||
RPCConversationHost string
|
||||
RPCFriendHost string
|
||||
RPCGroupHost string
|
||||
RPCMsgHost string
|
||||
RPCThirdHost string
|
||||
RPCUserHost string
|
||||
// Add other configuration fields as needed
|
||||
}
|
||||
|
||||
// LoadConfig loads the configurations from environment variables or default values.
|
||||
func LoadConfig() *Config {
|
||||
return &Config{
|
||||
APIHost: getEnv("OPENIM_API_HOST", "127.0.0.1"),
|
||||
APIPort: getEnv("API_OPENIM_PORT", "10002"),
|
||||
|
||||
// TODO: Set default variable
|
||||
MsgGatewayHost: getEnv("OPENIM_MSGGATEWAY_HOST", "default-msggateway-host"),
|
||||
MsgTransferHost: getEnv("OPENIM_MSGTRANSFER_HOST", "default-msgtransfer-host"),
|
||||
PushHost: getEnv("OPENIM_PUSH_HOST", "default-push-host"),
|
||||
RPCAuthHost: getEnv("OPENIM_RPC_AUTH_HOST", "default-rpc-auth-host"),
|
||||
RPCConversationHost: getEnv("OPENIM_RPC_CONVERSATION_HOST", "default-rpc-conversation-host"),
|
||||
RPCFriendHost: getEnv("OPENIM_RPC_FRIEND_HOST", "default-rpc-friend-host"),
|
||||
RPCGroupHost: getEnv("OPENIM_RPC_GROUP_HOST", "default-rpc-group-host"),
|
||||
RPCMsgHost: getEnv("OPENIM_RPC_MSG_HOST", "default-rpc-msg-host"),
|
||||
RPCThirdHost: getEnv("OPENIM_RPC_THIRD_HOST", "default-rpc-third-host"),
|
||||
RPCUserHost: getEnv("OPENIM_RPC_USER_HOST", "default-rpc-user-host"),
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv is a helper function to read an environment variable or return a default value.
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value, exists := os.LookupEnv(key)
|
||||
if !exists {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
89
test/e2e/framework/config/config_test.go
Normal file
89
test/e2e/framework/config/config_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyFlags(t *testing.T) {
|
||||
type args struct {
|
||||
source *flag.FlagSet
|
||||
target *flag.FlagSet
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Copy empty source to empty target",
|
||||
args: args{
|
||||
source: flag.NewFlagSet("source", flag.ContinueOnError),
|
||||
target: flag.NewFlagSet("target", flag.ContinueOnError),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Copy non-empty source to empty target",
|
||||
args: args{
|
||||
source: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("source", flag.ContinueOnError)
|
||||
fs.String("test-flag", "default", "test usage")
|
||||
return fs
|
||||
}(),
|
||||
target: flag.NewFlagSet("target", flag.ContinueOnError),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Copy source to target with existing flag",
|
||||
args: args{
|
||||
source: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("source", flag.ContinueOnError)
|
||||
fs.String("test-flag", "default", "test usage")
|
||||
return fs
|
||||
}(),
|
||||
target: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("target", flag.ContinueOnError)
|
||||
fs.String("test-flag", "default", "test usage")
|
||||
return fs
|
||||
}(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); (r != nil) != tt.wantErr {
|
||||
t.Errorf("CopyFlags() panic = %v, wantErr %v", r, tt.wantErr)
|
||||
}
|
||||
}()
|
||||
CopyFlags(tt.args.source, tt.args.target)
|
||||
|
||||
// Verify the replicated tag
|
||||
if !tt.wantErr {
|
||||
tt.args.source.VisitAll(func(f *flag.Flag) {
|
||||
if gotFlag := tt.args.target.Lookup(f.Name); gotFlag == nil || !reflect.DeepEqual(gotFlag, f) {
|
||||
t.Errorf("CopyFlags() failed to copy flag %s", f.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1
test/e2e/framework/ginkgowrapper/.keep
Normal file
1
test/e2e/framework/ginkgowrapper/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
15
test/e2e/framework/ginkgowrapper/ginkgowrapper.go
Normal file
15
test/e2e/framework/ginkgowrapper/ginkgowrapper.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ginkgowrapper
|
||||
15
test/e2e/framework/ginkgowrapper/ginkgowrapper_test.go
Normal file
15
test/e2e/framework/ginkgowrapper/ginkgowrapper_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ginkgowrapper
|
||||
1
test/e2e/framework/helpers/.keep
Normal file
1
test/e2e/framework/helpers/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
167
test/e2e/framework/helpers/chat/chat.go
Normal file
167
test/e2e/framework/helpers/chat/chat.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
// The default template version.
|
||||
defaultTemplateVersion = "v1.3.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define the URL to get the latest version
|
||||
// latestVersionURL := "https://github.com/openimsdk/chat/releases/latest"
|
||||
// latestVersion, err := getLatestVersion(latestVersionURL)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Failed to get the latest version: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
latestVersion := defaultTemplateVersion
|
||||
|
||||
// getLatestVersion
|
||||
|
||||
// Construct the download URL
|
||||
downloadURL := fmt.Sprintf("https://github.com/openimsdk/chat/releases/download/%s/chat_Linux_x86_64.tar.gz", latestVersion)
|
||||
|
||||
// Set the installation directory
|
||||
installDir := "/tmp/chat"
|
||||
|
||||
// Clear the installation directory before proceeding
|
||||
err := os.RemoveAll(installDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to clear installation directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create the installation directory
|
||||
err = os.MkdirAll(installDir, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create installation directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Download and extract OpenIM Chat to the installation directory
|
||||
err = downloadAndExtract(downloadURL, installDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to download and extract OpenIM Chat: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create configuration file directory
|
||||
configDir := filepath.Join(installDir, "config")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create configuration directory: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Download configuration files
|
||||
configURL := "https://raw.githubusercontent.com/openimsdk/chat/main/config/config.yaml"
|
||||
err = downloadAndExtract(configURL, configDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to download and extract configuration files: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Define the processes to be started
|
||||
cmds := []string{
|
||||
"admin-api",
|
||||
"admin-rpc",
|
||||
"chat-api",
|
||||
"chat-rpc",
|
||||
}
|
||||
|
||||
// Start each process in a new goroutine
|
||||
for _, cmd := range cmds {
|
||||
go startProcess(filepath.Join(installDir, cmd))
|
||||
}
|
||||
|
||||
// Block the main thread indefinitely
|
||||
select {}
|
||||
}
|
||||
|
||||
/* func getLatestVersion(url string) (string, error) {
|
||||
resp, err := webhook.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// location := resp.Header.Get("Location")
|
||||
// if location == "" {
|
||||
// return defaultTemplateVersion, nil
|
||||
// }
|
||||
|
||||
// Extract the version number from the URL
|
||||
latestVersion := filepath.Base(location)
|
||||
return latestVersion, nil
|
||||
} */
|
||||
|
||||
// downloadAndExtract downloads a file from a URL and extracts it to a destination directory.
|
||||
func downloadAndExtract(url, destDir string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("error downloading file, HTTP status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Create the destination directory
|
||||
err = os.MkdirAll(destDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Define the path for the downloaded file
|
||||
filePath := filepath.Join(destDir, "downloaded_file.tar.gz")
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the downloaded file
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract the file
|
||||
cmd := exec.Command("tar", "xzvf", filePath, "-C", destDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// startProcess starts a process and prints any errors encountered.
|
||||
func startProcess(cmdPath string) {
|
||||
cmd := exec.Command(cmdPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Failed to start process %s: %v\n", cmdPath, err)
|
||||
}
|
||||
}
|
||||
15
test/e2e/page/chat_page.go
Normal file
15
test/e2e/page/chat_page.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © 2024 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package page
|
||||
15
test/e2e/page/login_page.go
Normal file
15
test/e2e/page/login_page.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright © 2024 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package page
|
||||
1
test/e2e/performance/.keep
Normal file
1
test/e2e/performance/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/rpc/auth/.keep
Normal file
1
test/e2e/rpc/auth/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/rpc/conversation/.keep
Normal file
1
test/e2e/rpc/conversation/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/rpc/friend/.keep
Normal file
1
test/e2e/rpc/friend/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/rpc/group/.keep
Normal file
1
test/e2e/rpc/group/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/rpc/message/.keep
Normal file
1
test/e2e/rpc/message/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/scalability/.keep
Normal file
1
test/e2e/scalability/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
1
test/e2e/upgrade/.keep
Normal file
1
test/e2e/upgrade/.keep
Normal file
@@ -0,0 +1 @@
|
||||
.keep
|
||||
2
test/e2e/web/Readme.md
Normal file
2
test/e2e/web/Readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# OpenIM Web E2E
|
||||
|
||||
48
test/jwt/main.go
Normal file
48
test/jwt/main.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2023 OpenIM. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rawJWT := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiI4MjkzODEzMTgzIiwiUGxhdGZvcm1JRCI6NSwiZXhwIjoxNzA2NTk0MTU0LCJuYmYiOjE2OTg4MTc4NTQsImlhdCI6MTY5ODgxODE1NH0.QCJHzU07SC6iYBoFO6Zsm61TNDor2D89I4E3zg8HHHU`
|
||||
|
||||
// Verify the token
|
||||
claims := &jwt.MapClaims{}
|
||||
parsedT, err := jwt.ParseWithClaims(rawJWT, claims, func(token *jwt.Token) (any, error) {
|
||||
// Validate the alg is HMAC signature
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
if kid, ok := token.Header["kid"].(string); ok {
|
||||
fmt.Println("kid", kid)
|
||||
}
|
||||
|
||||
return []byte("key1"), nil
|
||||
})
|
||||
|
||||
if err != nil || !parsedT.Valid {
|
||||
fmt.Println("token valid failed", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("ok")
|
||||
}
|
||||
17
test/readme
Normal file
17
test/readme
Normal file
@@ -0,0 +1,17 @@
|
||||
## Run the Tests
|
||||
|
||||
read: [Test Docs](./docs/contrib/test.md)
|
||||
|
||||
To run a single test or set of tests, you'll need the [Ginkgo](https://github.com/onsi/ginkgo) tool installed on your
|
||||
machine:
|
||||
|
||||
```console
|
||||
go install github.com/onsi/ginkgo/ginkgo@latest
|
||||
```
|
||||
|
||||
```shell
|
||||
ginkgo --help
|
||||
--focus value
|
||||
If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.
|
||||
|
||||
```
|
||||
19
test/stress-test-v2/README.md
Normal file
19
test/stress-test-v2/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Stress Test V2
|
||||
|
||||
## Usage
|
||||
|
||||
You need set `TestTargetUserList` variables.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
||||
go build -o test/stress-test-v2/stress-test-v2 test/stress-test-v2/main.go
|
||||
```
|
||||
|
||||
### Excute
|
||||
|
||||
```bash
|
||||
|
||||
tools/stress-test-v2/stress-test-v2 -c config/
|
||||
```
|
||||
759
test/stress-test-v2/main.go
Normal file
759
test/stress-test-v2/main.go
Normal file
@@ -0,0 +1,759 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/protocol/auth"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
)
|
||||
|
||||
// 1. Create 100K New Users
|
||||
// 2. Create 100 100K Groups
|
||||
// 3. Create 1000 999 Groups
|
||||
// 4. Send message to 100K Groups every second
|
||||
// 5. Send message to 999 Groups every minute
|
||||
|
||||
var (
|
||||
// Use default userIDs List for testing, need to be created.
|
||||
TestTargetUserList = []string{
|
||||
// "<need-update-it>",
|
||||
}
|
||||
// DefaultGroupID = "<need-update-it>" // Use default group ID for testing, need to be created.
|
||||
)
|
||||
|
||||
var (
|
||||
ApiAddress string
|
||||
|
||||
// API method
|
||||
GetAdminToken = "/auth/get_admin_token"
|
||||
UserCheck = "/user/account_check"
|
||||
CreateUser = "/user/user_register"
|
||||
ImportFriend = "/friend/import_friend"
|
||||
InviteToGroup = "/group/invite_user_to_group"
|
||||
GetGroupMemberInfo = "/group/get_group_members_info"
|
||||
SendMsg = "/msg/send_msg"
|
||||
CreateGroup = "/group/create_group"
|
||||
GetUserToken = "/auth/user_token"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxUser = 100000
|
||||
Max1kUser = 1000
|
||||
Max100KGroup = 100
|
||||
Max999Group = 1000
|
||||
MaxInviteUserLimit = 999
|
||||
|
||||
CreateUserTicker = 1 * time.Second
|
||||
CreateGroupTicker = 1 * time.Second
|
||||
Create100KGroupTicker = 1 * time.Second
|
||||
Create999GroupTicker = 1 * time.Second
|
||||
SendMsgTo100KGroupTicker = 1 * time.Second
|
||||
SendMsgTo999GroupTicker = 1 * time.Minute
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
ErrCode int `json:"errCode"`
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type StressTest struct {
|
||||
Conf *conf
|
||||
AdminUserID string
|
||||
AdminToken string
|
||||
DefaultGroupID string
|
||||
DefaultUserID string
|
||||
UserCounter int
|
||||
CreateUserCounter int
|
||||
Create100kGroupCounter int
|
||||
Create999GroupCounter int
|
||||
MsgCounter int
|
||||
CreatedUsers []string
|
||||
CreatedGroups []string
|
||||
Mutex sync.Mutex
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
HttpClient *http.Client
|
||||
Wg sync.WaitGroup
|
||||
Once sync.Once
|
||||
}
|
||||
|
||||
type conf struct {
|
||||
Share config.Share
|
||||
Api config.API
|
||||
}
|
||||
|
||||
func initConfig(configDir string) (*config.Share, *config.API, error) {
|
||||
var (
|
||||
share = &config.Share{}
|
||||
apiConfig = &config.API{}
|
||||
)
|
||||
|
||||
err := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return share, apiConfig, nil
|
||||
}
|
||||
|
||||
// Post Request
|
||||
func (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) {
|
||||
// Marshal body
|
||||
jsonBody, err := json.Marshal(reqbody)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to marshal request body", err, "url", url, "reqbody", reqbody)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("operationID", st.AdminUserID)
|
||||
if st.AdminToken != "" {
|
||||
req.Header.Set("token", st.AdminToken)
|
||||
}
|
||||
|
||||
// log.ZInfo(ctx, "Header info is ", "Content-Type", "application/json", "operationID", st.AdminUserID, "token", st.AdminToken)
|
||||
|
||||
resp, err := st.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to read response body", err, "url", url)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var baseResp BaseResp
|
||||
if err := json.Unmarshal(respBody, &baseResp); err != nil {
|
||||
log.ZError(ctx, "Failed to unmarshal response body", err, "url", url, "respBody", string(respBody))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if baseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf(baseResp.ErrMsg)
|
||||
// log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseResp.Data, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) GetAdminToken(ctx context.Context) (string, error) {
|
||||
req := auth.GetAdminTokenReq{
|
||||
Secret: st.Conf.Share.Secret,
|
||||
UserID: st.AdminUserID,
|
||||
}
|
||||
|
||||
resp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := &auth.GetAdminTokenResp{}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return data.Token, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) CheckUser(ctx context.Context, userIDs []string) ([]string, error) {
|
||||
req := pbuser.AccountCheckReq{
|
||||
CheckUserIDs: userIDs,
|
||||
}
|
||||
|
||||
resp, err := st.PostRequest(ctx, ApiAddress+UserCheck, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &pbuser.AccountCheckResp{}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unRegisteredUserIDs := make([]string, 0)
|
||||
|
||||
for _, res := range data.Results {
|
||||
if res.AccountStatus == constant.UnRegistered {
|
||||
unRegisteredUserIDs = append(unRegisteredUserIDs, res.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
return unRegisteredUserIDs, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) {
|
||||
user := &sdkws.UserInfo{
|
||||
UserID: userID,
|
||||
Nickname: userID,
|
||||
}
|
||||
|
||||
req := pbuser.UserRegisterReq{
|
||||
Users: []*sdkws.UserInfo{user},
|
||||
}
|
||||
|
||||
_, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
st.UserCounter++
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) CreateUserBatch(ctx context.Context, userIDs []string) error {
|
||||
// The method can import a large number of users at once.
|
||||
var userList []*sdkws.UserInfo
|
||||
|
||||
defer st.Once.Do(
|
||||
func() {
|
||||
st.DefaultUserID = userIDs[0]
|
||||
fmt.Println("Default Send User Created ID:", st.DefaultUserID)
|
||||
})
|
||||
|
||||
needUserIDs, err := st.CheckUser(ctx, userIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, userID := range needUserIDs {
|
||||
user := &sdkws.UserInfo{
|
||||
UserID: userID,
|
||||
Nickname: userID,
|
||||
}
|
||||
userList = append(userList, user)
|
||||
}
|
||||
|
||||
req := pbuser.UserRegisterReq{
|
||||
Users: userList,
|
||||
}
|
||||
|
||||
_, err = st.PostRequest(ctx, ApiAddress+CreateUser, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st.UserCounter += len(userList)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *StressTest) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]string, error) {
|
||||
needInviteUserIDs := make([]string, 0)
|
||||
|
||||
const maxBatchSize = 500
|
||||
if len(userIDs) > maxBatchSize {
|
||||
for i := 0; i < len(userIDs); i += maxBatchSize {
|
||||
end := min(i+maxBatchSize, len(userIDs))
|
||||
batchUserIDs := userIDs[i:end]
|
||||
|
||||
// log.ZInfo(ctx, "Processing group members batch", "groupID", groupID, "batch", i/maxBatchSize+1,
|
||||
// "batchUserCount", len(batchUserIDs))
|
||||
|
||||
// Process a single batch
|
||||
batchReq := group.GetGroupMembersInfoReq{
|
||||
GroupID: groupID,
|
||||
UserIDs: batchUserIDs,
|
||||
}
|
||||
|
||||
resp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &batchReq)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Batch query failed", err, "batch", i/maxBatchSize+1)
|
||||
continue
|
||||
}
|
||||
|
||||
data := &group.GetGroupMembersInfoResp{}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
log.ZError(ctx, "Failed to parse batch response", err, "batch", i/maxBatchSize+1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the batch results
|
||||
existingMembers := make(map[string]bool)
|
||||
for _, member := range data.Members {
|
||||
existingMembers[member.UserID] = true
|
||||
}
|
||||
|
||||
for _, userID := range batchUserIDs {
|
||||
if !existingMembers[userID] {
|
||||
needInviteUserIDs = append(needInviteUserIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return needInviteUserIDs, nil
|
||||
}
|
||||
|
||||
req := group.GetGroupMembersInfoReq{
|
||||
GroupID: groupID,
|
||||
UserIDs: userIDs,
|
||||
}
|
||||
|
||||
resp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &group.GetGroupMembersInfoResp{}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMembers := make(map[string]bool)
|
||||
for _, member := range data.Members {
|
||||
existingMembers[member.UserID] = true
|
||||
}
|
||||
|
||||
for _, userID := range userIDs {
|
||||
if !existingMembers[userID] {
|
||||
needInviteUserIDs = append(needInviteUserIDs, userID)
|
||||
}
|
||||
}
|
||||
|
||||
return needInviteUserIDs, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) InviteToGroup(ctx context.Context, groupID string, userIDs []string) error {
|
||||
req := group.InviteUserToGroupReq{
|
||||
GroupID: groupID,
|
||||
InvitedUserIDs: userIDs,
|
||||
}
|
||||
_, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *StressTest) SendMsg(ctx context.Context, userID string, groupID string) error {
|
||||
contentObj := map[string]any{
|
||||
// "content": fmt.Sprintf("index %d. The current time is %s", st.MsgCounter, time.Now().Format("2006-01-02 15:04:05.000")),
|
||||
"content": fmt.Sprintf("The current time is %s", time.Now().Format("2006-01-02 15:04:05.000")),
|
||||
}
|
||||
|
||||
req := &apistruct.SendMsgReq{
|
||||
SendMsg: apistruct.SendMsg{
|
||||
SendID: userID,
|
||||
SenderNickname: userID,
|
||||
GroupID: groupID,
|
||||
ContentType: constant.Text,
|
||||
SessionType: constant.ReadGroupChatType,
|
||||
Content: contentObj,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to send message", err, "userID", userID, "req", &req)
|
||||
return err
|
||||
}
|
||||
|
||||
st.MsgCounter++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Max userIDs number is 1000
|
||||
func (st *StressTest) CreateGroup(ctx context.Context, groupID string, userID string, userIDsList []string) (string, error) {
|
||||
groupInfo := &sdkws.GroupInfo{
|
||||
GroupID: groupID,
|
||||
GroupName: groupID,
|
||||
GroupType: constant.WorkingGroup,
|
||||
}
|
||||
|
||||
req := group.CreateGroupReq{
|
||||
OwnerUserID: userID,
|
||||
MemberUserIDs: userIDsList,
|
||||
GroupInfo: groupInfo,
|
||||
}
|
||||
|
||||
resp := group.CreateGroupResp{}
|
||||
|
||||
response, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(response, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// st.GroupCounter++
|
||||
|
||||
return resp.GroupInfo.GroupID, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
// defaultConfigDir := filepath.Join("..", "..", "..", "..", "..", "config")
|
||||
// flag.StringVar(&configPath, "c", defaultConfigDir, "config path")
|
||||
flag.StringVar(&configPath, "c", "", "config path")
|
||||
flag.Parse()
|
||||
|
||||
if configPath == "" {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "config path is empty")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Config Path: %s\n", configPath)
|
||||
|
||||
share, apiConfig, err := initConfig(configPath)
|
||||
if err != nil {
|
||||
program.ExitWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ApiAddress = fmt.Sprintf("http://%s:%s", "127.0.0.1", fmt.Sprint(apiConfig.Api.Ports[0]))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// ch := make(chan struct{})
|
||||
|
||||
st := &StressTest{
|
||||
Conf: &conf{
|
||||
Share: *share,
|
||||
Api: *apiConfig,
|
||||
},
|
||||
AdminUserID: share.IMAdminUser.UserIDs[0],
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
HttpClient: &http.Client{
|
||||
Timeout: 50 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
fmt.Println("\nReceived stop signal, stopping...")
|
||||
|
||||
go func() {
|
||||
// time.Sleep(5 * time.Second)
|
||||
fmt.Println("Force exit")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
st.Cancel()
|
||||
}()
|
||||
|
||||
token, err := st.GetAdminToken(st.Ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Get Admin Token failed.", err, "AdminUserID", st.AdminUserID)
|
||||
}
|
||||
|
||||
st.AdminToken = token
|
||||
fmt.Println("Admin Token:", st.AdminToken)
|
||||
fmt.Println("ApiAddress:", ApiAddress)
|
||||
|
||||
for i := range MaxUser {
|
||||
userID := fmt.Sprintf("v2_StressTest_User_%d", i)
|
||||
st.CreatedUsers = append(st.CreatedUsers, userID)
|
||||
st.CreateUserCounter++
|
||||
}
|
||||
|
||||
// err = st.CreateUserBatch(st.Ctx, st.CreatedUsers)
|
||||
// if err != nil {
|
||||
// log.ZError(ctx, "Create user failed.", err)
|
||||
// }
|
||||
|
||||
const batchSize = 1000
|
||||
totalUsers := len(st.CreatedUsers)
|
||||
successCount := 0
|
||||
|
||||
if st.DefaultUserID == "" && len(st.CreatedUsers) > 0 {
|
||||
st.DefaultUserID = st.CreatedUsers[0]
|
||||
}
|
||||
|
||||
for i := 0; i < totalUsers; i += batchSize {
|
||||
end := min(i+batchSize, totalUsers)
|
||||
|
||||
userBatch := st.CreatedUsers[i:end]
|
||||
log.ZInfo(st.Ctx, "Creating user batch", "batch", i/batchSize+1, "count", len(userBatch))
|
||||
|
||||
err = st.CreateUserBatch(st.Ctx, userBatch)
|
||||
if err != nil {
|
||||
log.ZError(st.Ctx, "Batch user creation failed", err, "batch", i/batchSize+1)
|
||||
} else {
|
||||
successCount += len(userBatch)
|
||||
log.ZInfo(st.Ctx, "Batch user creation succeeded", "batch", i/batchSize+1,
|
||||
"progress", fmt.Sprintf("%d/%d", successCount, totalUsers))
|
||||
}
|
||||
}
|
||||
|
||||
// Execute create 100k group
|
||||
st.Wg.Add(1)
|
||||
go func() {
|
||||
defer st.Wg.Done()
|
||||
|
||||
create100kGroupTicker := time.NewTicker(Create100KGroupTicker)
|
||||
defer create100kGroupTicker.Stop()
|
||||
|
||||
for i := range Max100KGroup {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Create 100K Group")
|
||||
return
|
||||
|
||||
case <-create100kGroupTicker.C:
|
||||
// Create 100K groups
|
||||
st.Wg.Add(1)
|
||||
go func(idx int) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
elapsedTime := time.Since(startTime)
|
||||
log.ZInfo(st.Ctx, "100K group creation completed",
|
||||
"groupID", fmt.Sprintf("v2_StressTest_Group_100K_%d", idx),
|
||||
"index", idx,
|
||||
"duration", elapsedTime.String())
|
||||
}()
|
||||
|
||||
defer st.Wg.Done()
|
||||
defer func() {
|
||||
st.Mutex.Lock()
|
||||
st.Create100kGroupCounter++
|
||||
st.Mutex.Unlock()
|
||||
}()
|
||||
|
||||
groupID := fmt.Sprintf("v2_StressTest_Group_100K_%d", idx)
|
||||
|
||||
if _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {
|
||||
log.ZError(st.Ctx, "Create group failed.", err)
|
||||
// continue
|
||||
}
|
||||
|
||||
for i := 0; i <= MaxUser/MaxInviteUserLimit; i++ {
|
||||
InviteUserIDs := make([]string, 0)
|
||||
// ensure TargetUserList is in group
|
||||
InviteUserIDs = append(InviteUserIDs, TestTargetUserList...)
|
||||
|
||||
startIdx := max(i*MaxInviteUserLimit, 1)
|
||||
endIdx := min((i+1)*MaxInviteUserLimit, MaxUser)
|
||||
|
||||
for j := startIdx; j < endIdx; j++ {
|
||||
userCreatedID := fmt.Sprintf("v2_StressTest_User_%d", j)
|
||||
InviteUserIDs = append(InviteUserIDs, userCreatedID)
|
||||
}
|
||||
|
||||
if len(InviteUserIDs) == 0 {
|
||||
// log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
InviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)
|
||||
if err != nil {
|
||||
log.ZError(st.Ctx, "GetGroupMembersInfo failed.", err, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(InviteUserIDs) == 0 {
|
||||
// log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Invite To Group
|
||||
if err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {
|
||||
log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", InviteUserIDs)
|
||||
continue
|
||||
// os.Exit(1)
|
||||
// return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// create 999 groups
|
||||
st.Wg.Add(1)
|
||||
go func() {
|
||||
defer st.Wg.Done()
|
||||
|
||||
create999GroupTicker := time.NewTicker(Create999GroupTicker)
|
||||
defer create999GroupTicker.Stop()
|
||||
|
||||
for i := range Max999Group {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Create 999 Group")
|
||||
return
|
||||
|
||||
case <-create999GroupTicker.C:
|
||||
// Create 999 groups
|
||||
st.Wg.Add(1)
|
||||
go func(idx int) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
elapsedTime := time.Since(startTime)
|
||||
log.ZInfo(st.Ctx, "999 group creation completed",
|
||||
"groupID", fmt.Sprintf("v2_StressTest_Group_1K_%d", idx),
|
||||
"index", idx,
|
||||
"duration", elapsedTime.String())
|
||||
}()
|
||||
|
||||
defer st.Wg.Done()
|
||||
defer func() {
|
||||
st.Mutex.Lock()
|
||||
st.Create999GroupCounter++
|
||||
st.Mutex.Unlock()
|
||||
}()
|
||||
|
||||
groupID := fmt.Sprintf("v2_StressTest_Group_1K_%d", idx)
|
||||
|
||||
if _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {
|
||||
log.ZError(st.Ctx, "Create group failed.", err)
|
||||
// continue
|
||||
}
|
||||
for i := 0; i <= Max1kUser/MaxInviteUserLimit; i++ {
|
||||
InviteUserIDs := make([]string, 0)
|
||||
// ensure TargetUserList is in group
|
||||
InviteUserIDs = append(InviteUserIDs, TestTargetUserList...)
|
||||
|
||||
startIdx := max(i*MaxInviteUserLimit, 1)
|
||||
endIdx := min((i+1)*MaxInviteUserLimit, Max1kUser)
|
||||
|
||||
for j := startIdx; j < endIdx; j++ {
|
||||
userCreatedID := fmt.Sprintf("v2_StressTest_User_%d", j)
|
||||
InviteUserIDs = append(InviteUserIDs, userCreatedID)
|
||||
}
|
||||
|
||||
if len(InviteUserIDs) == 0 {
|
||||
// log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
InviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)
|
||||
if err != nil {
|
||||
log.ZError(st.Ctx, "GetGroupMembersInfo failed.", err, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(InviteUserIDs) == 0 {
|
||||
// log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Invite To Group
|
||||
if err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {
|
||||
log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", InviteUserIDs)
|
||||
continue
|
||||
// os.Exit(1)
|
||||
// return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send message to 100K groups
|
||||
st.Wg.Wait()
|
||||
fmt.Println("All groups created successfully, starting to send messages...")
|
||||
log.ZInfo(ctx, "All groups created successfully, starting to send messages...")
|
||||
|
||||
var groups100K []string
|
||||
var groups999 []string
|
||||
|
||||
for i := range Max100KGroup {
|
||||
groupID := fmt.Sprintf("v2_StressTest_Group_100K_%d", i)
|
||||
groups100K = append(groups100K, groupID)
|
||||
}
|
||||
|
||||
for i := range Max999Group {
|
||||
groupID := fmt.Sprintf("v2_StressTest_Group_1K_%d", i)
|
||||
groups999 = append(groups999, groupID)
|
||||
}
|
||||
|
||||
send100kGroupLimiter := make(chan struct{}, 20)
|
||||
send999GroupLimiter := make(chan struct{}, 100)
|
||||
|
||||
// execute Send message to 100K groups
|
||||
go func() {
|
||||
ticker := time.NewTicker(SendMsgTo100KGroupTicker)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Send Message to 100K Group")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Send message to 100K groups
|
||||
for _, groupID := range groups100K {
|
||||
send100kGroupLimiter <- struct{}{}
|
||||
go func(groupID string) {
|
||||
defer func() { <-send100kGroupLimiter }()
|
||||
if err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {
|
||||
log.ZError(st.Ctx, "Send message to 100K group failed.", err)
|
||||
}
|
||||
}(groupID)
|
||||
}
|
||||
// log.ZInfo(st.Ctx, "Send message to 100K groups successfully.")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// execute Send message to 999 groups
|
||||
go func() {
|
||||
ticker := time.NewTicker(SendMsgTo999GroupTicker)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Send Message to 999 Group")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Send message to 999 groups
|
||||
for _, groupID := range groups999 {
|
||||
send999GroupLimiter <- struct{}{}
|
||||
go func(groupID string) {
|
||||
defer func() { <-send999GroupLimiter }()
|
||||
|
||||
if err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {
|
||||
log.ZError(st.Ctx, "Send message to 999 group failed.", err)
|
||||
}
|
||||
}(groupID)
|
||||
}
|
||||
// log.ZInfo(st.Ctx, "Send message to 999 groups successfully.")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-st.Ctx.Done()
|
||||
fmt.Println("Received signal to exit, shutting down...")
|
||||
}
|
||||
19
test/stress-test/README.md
Normal file
19
test/stress-test/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Stress Test
|
||||
|
||||
## Usage
|
||||
|
||||
You need set `TestTargetUserList` and `DefaultGroupID` variables.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
||||
go build -o test/stress-test/stress-test test/stress-test/main.go
|
||||
```
|
||||
|
||||
### Excute
|
||||
|
||||
```bash
|
||||
|
||||
tools/stress-test/stress-test -c config/
|
||||
```
|
||||
458
test/stress-test/main.go
Executable file
458
test/stress-test/main.go
Executable file
@@ -0,0 +1,458 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/protocol/auth"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
)
|
||||
|
||||
/*
|
||||
1. Create one user every minute
|
||||
2. Import target users as friends
|
||||
3. Add users to the default group
|
||||
4. Send a message to the default group every second, containing index and current timestamp
|
||||
5. Create a new group every minute and invite target users to join
|
||||
*/
|
||||
|
||||
// !!! ATTENTION: This variable is must be added!
|
||||
var (
|
||||
// Use default userIDs List for testing, need to be created.
|
||||
TestTargetUserList = []string{
|
||||
"<need-update-it>",
|
||||
}
|
||||
DefaultGroupID = "<need-update-it>" // Use default group ID for testing, need to be created.
|
||||
)
|
||||
|
||||
var (
|
||||
ApiAddress string
|
||||
|
||||
// API method
|
||||
GetAdminToken = "/auth/get_admin_token"
|
||||
CreateUser = "/user/user_register"
|
||||
ImportFriend = "/friend/import_friend"
|
||||
InviteToGroup = "/group/invite_user_to_group"
|
||||
SendMsg = "/msg/send_msg"
|
||||
CreateGroup = "/group/create_group"
|
||||
GetUserToken = "/auth/user_token"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxUser = 10000
|
||||
MaxGroup = 1000
|
||||
|
||||
CreateUserTicker = 1 * time.Minute // Ticker is 1min in create user
|
||||
SendMessageTicker = 1 * time.Second // Ticker is 1s in send message
|
||||
CreateGroupTicker = 1 * time.Minute
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
ErrCode int `json:"errCode"`
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type StressTest struct {
|
||||
Conf *conf
|
||||
AdminUserID string
|
||||
AdminToken string
|
||||
DefaultGroupID string
|
||||
DefaultUserID string
|
||||
UserCounter int
|
||||
GroupCounter int
|
||||
MsgCounter int
|
||||
CreatedUsers []string
|
||||
CreatedGroups []string
|
||||
Mutex sync.Mutex
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
HttpClient *http.Client
|
||||
Wg sync.WaitGroup
|
||||
Once sync.Once
|
||||
}
|
||||
|
||||
type conf struct {
|
||||
Share config.Share
|
||||
Api config.API
|
||||
}
|
||||
|
||||
func initConfig(configDir string) (*config.Share, *config.API, error) {
|
||||
var (
|
||||
share = &config.Share{}
|
||||
apiConfig = &config.API{}
|
||||
)
|
||||
|
||||
err := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return share, apiConfig, nil
|
||||
}
|
||||
|
||||
// Post Request
|
||||
func (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) {
|
||||
// Marshal body
|
||||
jsonBody, err := json.Marshal(reqbody)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to marshal request body", err, "url", url, "reqbody", reqbody)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("operationID", st.AdminUserID)
|
||||
if st.AdminToken != "" {
|
||||
req.Header.Set("token", st.AdminToken)
|
||||
}
|
||||
|
||||
// log.ZInfo(ctx, "Header info is ", "Content-Type", "application/json", "operationID", st.AdminUserID, "token", st.AdminToken)
|
||||
|
||||
resp, err := st.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to read response body", err, "url", url)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var baseResp BaseResp
|
||||
if err := json.Unmarshal(respBody, &baseResp); err != nil {
|
||||
log.ZError(ctx, "Failed to unmarshal response body", err, "url", url, "respBody", string(respBody))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if baseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf(baseResp.ErrMsg)
|
||||
log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return baseResp.Data, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) GetAdminToken(ctx context.Context) (string, error) {
|
||||
req := auth.GetAdminTokenReq{
|
||||
Secret: st.Conf.Share.Secret,
|
||||
UserID: st.AdminUserID,
|
||||
}
|
||||
|
||||
resp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := &auth.GetAdminTokenResp{}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return data.Token, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) {
|
||||
user := &sdkws.UserInfo{
|
||||
UserID: userID,
|
||||
Nickname: userID,
|
||||
}
|
||||
|
||||
req := pbuser.UserRegisterReq{
|
||||
Users: []*sdkws.UserInfo{user},
|
||||
}
|
||||
|
||||
_, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
st.UserCounter++
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (st *StressTest) ImportFriend(ctx context.Context, userID string) error {
|
||||
req := relation.ImportFriendReq{
|
||||
OwnerUserID: userID,
|
||||
FriendUserIDs: TestTargetUserList,
|
||||
}
|
||||
|
||||
_, err := st.PostRequest(ctx, ApiAddress+ImportFriend, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *StressTest) InviteToGroup(ctx context.Context, userID string) error {
|
||||
req := group.InviteUserToGroupReq{
|
||||
GroupID: st.DefaultGroupID,
|
||||
InvitedUserIDs: []string{userID},
|
||||
}
|
||||
_, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *StressTest) SendMsg(ctx context.Context, userID string) error {
|
||||
contentObj := map[string]any{
|
||||
"content": fmt.Sprintf("index %d. The current time is %s", st.MsgCounter, time.Now().Format("2006-01-02 15:04:05.000")),
|
||||
}
|
||||
|
||||
req := &apistruct.SendMsgReq{
|
||||
SendMsg: apistruct.SendMsg{
|
||||
SendID: userID,
|
||||
SenderNickname: userID,
|
||||
GroupID: st.DefaultGroupID,
|
||||
ContentType: constant.Text,
|
||||
SessionType: constant.ReadGroupChatType,
|
||||
Content: contentObj,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Failed to send message", err, "userID", userID, "req", &req)
|
||||
return err
|
||||
}
|
||||
|
||||
st.MsgCounter++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *StressTest) CreateGroup(ctx context.Context, userID string) (string, error) {
|
||||
groupID := fmt.Sprintf("StressTestGroup_%d_%s", st.GroupCounter, time.Now().Format("20060102150405"))
|
||||
|
||||
groupInfo := &sdkws.GroupInfo{
|
||||
GroupID: groupID,
|
||||
GroupName: groupID,
|
||||
GroupType: constant.WorkingGroup,
|
||||
}
|
||||
|
||||
req := group.CreateGroupReq{
|
||||
OwnerUserID: userID,
|
||||
MemberUserIDs: TestTargetUserList,
|
||||
GroupInfo: groupInfo,
|
||||
}
|
||||
|
||||
resp := group.CreateGroupResp{}
|
||||
|
||||
response, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(response, &resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
st.GroupCounter++
|
||||
|
||||
return resp.GroupInfo.GroupID, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
// defaultConfigDir := filepath.Join("..", "..", "..", "..", "..", "config")
|
||||
// flag.StringVar(&configPath, "c", defaultConfigDir, "config path")
|
||||
flag.StringVar(&configPath, "c", "", "config path")
|
||||
flag.Parse()
|
||||
|
||||
if configPath == "" {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "config path is empty")
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Config Path: %s\n", configPath)
|
||||
|
||||
share, apiConfig, err := initConfig(configPath)
|
||||
if err != nil {
|
||||
program.ExitWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ApiAddress = fmt.Sprintf("http://%s:%s", "127.0.0.1", fmt.Sprint(apiConfig.Api.Ports[0]))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ch := make(chan struct{})
|
||||
|
||||
defer cancel()
|
||||
|
||||
st := &StressTest{
|
||||
Conf: &conf{
|
||||
Share: *share,
|
||||
Api: *apiConfig,
|
||||
},
|
||||
AdminUserID: share.IMAdminUser.UserIDs[0],
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
HttpClient: &http.Client{
|
||||
Timeout: 50 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
fmt.Println("\nReceived stop signal, stopping...")
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
close(ch)
|
||||
}
|
||||
|
||||
st.Cancel()
|
||||
}()
|
||||
|
||||
token, err := st.GetAdminToken(st.Ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Get Admin Token failed.", err, "AdminUserID", st.AdminUserID)
|
||||
}
|
||||
|
||||
st.AdminToken = token
|
||||
fmt.Println("Admin Token:", st.AdminToken)
|
||||
fmt.Println("ApiAddress:", ApiAddress)
|
||||
|
||||
st.DefaultGroupID = DefaultGroupID
|
||||
|
||||
st.Wg.Add(1)
|
||||
go func() {
|
||||
defer st.Wg.Done()
|
||||
|
||||
ticker := time.NewTicker(CreateUserTicker)
|
||||
defer ticker.Stop()
|
||||
|
||||
for st.UserCounter < MaxUser {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Create user", "reason", "context done")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Create User
|
||||
userID := fmt.Sprintf("%d_Stresstest_%s", st.UserCounter, time.Now().Format("0102150405"))
|
||||
|
||||
userCreatedID, err := st.CreateUser(st.Ctx, userID)
|
||||
if err != nil {
|
||||
log.ZError(st.Ctx, "Create User failed.", err, "UserID", userID)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
// fmt.Println("User Created ID:", userCreatedID)
|
||||
|
||||
// Import Friend
|
||||
if err = st.ImportFriend(st.Ctx, userCreatedID); err != nil {
|
||||
log.ZError(st.Ctx, "Import Friend failed.", err, "UserID", userCreatedID)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
// Invite To Group
|
||||
if err = st.InviteToGroup(st.Ctx, userCreatedID); err != nil {
|
||||
log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", userCreatedID)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
st.Once.Do(func() {
|
||||
st.DefaultUserID = userCreatedID
|
||||
fmt.Println("Default Send User Created ID:", userCreatedID)
|
||||
close(ch)
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
st.Wg.Add(1)
|
||||
go func() {
|
||||
defer st.Wg.Done()
|
||||
|
||||
ticker := time.NewTicker(SendMessageTicker)
|
||||
defer ticker.Stop()
|
||||
<-ch
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Send message", "reason", "context done")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Send Message
|
||||
if err = st.SendMsg(st.Ctx, st.DefaultUserID); err != nil {
|
||||
log.ZError(st.Ctx, "Send Message failed.", err, "UserID", st.DefaultUserID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
st.Wg.Add(1)
|
||||
go func() {
|
||||
defer st.Wg.Done()
|
||||
|
||||
ticker := time.NewTicker(CreateGroupTicker)
|
||||
defer ticker.Stop()
|
||||
<-ch
|
||||
|
||||
for st.GroupCounter < MaxGroup {
|
||||
|
||||
select {
|
||||
case <-st.Ctx.Done():
|
||||
log.ZInfo(st.Ctx, "Stop Create Group", "reason", "context done")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
|
||||
// Create Group
|
||||
_, err := st.CreateGroup(st.Ctx, st.DefaultUserID)
|
||||
if err != nil {
|
||||
log.ZError(st.Ctx, "Create Group failed.", err, "UserID", st.DefaultUserID)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
// fmt.Println("Group Created ID:", groupID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
st.Wg.Wait()
|
||||
}
|
||||
64
test/testdata/README.md
vendored
Normal file
64
test/testdata/README.md
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
# Test Data for OpenIM Server
|
||||
|
||||
This directory (`testdata`) contains various JSON formatted data files that are used for testing the OpenIM Server.
|
||||
|
||||
## Structure
|
||||
|
||||
```bash
|
||||
testdata/
|
||||
│
|
||||
├── README.md # 描述该目录下各子目录和文件的作用
|
||||
│
|
||||
├── storage/ # 存储模拟的数据库数据
|
||||
│ ├── users.json # 用户的模拟数据
|
||||
│ └── messages.json # 消息的模拟数据
|
||||
│
|
||||
├── requests/ # 存储模拟的请求数据
|
||||
│ ├── login.json # 模拟登陆请求
|
||||
│ ├── register.json # 模拟注册请求
|
||||
│ └── sendMessage.json # 模拟发送消息请求
|
||||
│
|
||||
└── responses/ # 存储模拟的响应数据
|
||||
├── login.json # 模拟登陆响应
|
||||
├── register.json # 模拟注册响应
|
||||
└── sendMessage.json # 模拟发送消息响应
|
||||
```
|
||||
|
||||
Here is an overview of what each subdirectory or file represents:
|
||||
|
||||
- `db/` - This directory contains mock data mimicking the actual database contents.
|
||||
- `users.json` - Represents a list of users in the system. Each entry contains user-specific information such as user ID, username, password hash, etc.
|
||||
- `messages.json` - Contains a list of messages exchanged between users. Each message entry includes the sender's and receiver's user IDs, message content, timestamp, etc.
|
||||
- `requests/` - This directory contains mock requests that a client might send to the server.
|
||||
- `login.json` - Represents a user login request. It includes fields such as username and password.
|
||||
- `register.json` - Mimics a user registration request. Contains details such as username, password, email, etc.
|
||||
- `sendMessage.json` - Simulates a message sending request from a user to another user.
|
||||
- `responses/` - This directory holds the expected server responses for the respective requests.
|
||||
- `login.json` - Represents a successful login response from the server. It typically includes a session token and user-specific information.
|
||||
- `register.json` - Simulates a successful registration response from the server, usually containing the new user's ID, username, etc.
|
||||
- `sendMessage.json` - Depicts a successful message sending response from the server, confirming the delivery of the message.
|
||||
|
||||
## JSON Format
|
||||
|
||||
All the data files in this directory are in JSON format. JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate.
|
||||
|
||||
Here is a simple example of what a JSON file might look like:
|
||||
|
||||
```bash
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "user1",
|
||||
"password": "password1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"username": "user2",
|
||||
"password": "password2"
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
In this example, "users" is an array of user objects. Each user object has an "id", "username", and "password".
|
||||
0
test/testdata/db/messages.json
vendored
Normal file
0
test/testdata/db/messages.json
vendored
Normal file
0
test/testdata/db/users.json
vendored
Normal file
0
test/testdata/db/users.json
vendored
Normal file
0
test/testdata/requests/login.json
vendored
Normal file
0
test/testdata/requests/login.json
vendored
Normal file
0
test/testdata/requests/register.json
vendored
Normal file
0
test/testdata/requests/register.json
vendored
Normal file
0
test/testdata/requests/send-message.json
vendored
Normal file
0
test/testdata/requests/send-message.json
vendored
Normal file
0
test/testdata/responses/login.json
vendored
Normal file
0
test/testdata/responses/login.json
vendored
Normal file
0
test/testdata/responses/register.json
vendored
Normal file
0
test/testdata/responses/register.json
vendored
Normal file
0
test/testdata/responses/sendMessage.json
vendored
Normal file
0
test/testdata/responses/sendMessage.json
vendored
Normal file
65
test/webhook/msgmodify/main.go
Normal file
65
test/webhook/msgmodify/main.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
g := gin.Default()
|
||||
g.POST("/callbackExample/callbackBeforeMsgModifyCommand", toGin(handlerMsg))
|
||||
if err := g.Run(":10006"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func toGin[R any](fn func(c *gin.Context, req *R)) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("HTTP %s %s %s\n", c.Request.Method, c.Request.URL, body)
|
||||
var req R
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
fn(c, &req)
|
||||
}
|
||||
}
|
||||
|
||||
func handlerMsg(c *gin.Context, req *cbapi.CallbackMsgModifyCommandReq) {
|
||||
var resp cbapi.CallbackMsgModifyCommandResp
|
||||
if req.ContentType != constant.Text {
|
||||
c.JSON(http.StatusOK, &resp)
|
||||
return
|
||||
}
|
||||
var textElem struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(req.Content), &textElem); err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
const word = "xxx"
|
||||
if strings.Contains(textElem.Content, word) {
|
||||
textElem.Content = strings.ReplaceAll(textElem.Content, word, strings.Repeat("*", len(word)))
|
||||
content, err := json.Marshal(&textElem)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
tmp := string(content)
|
||||
resp.Content = &tmp
|
||||
}
|
||||
c.JSON(http.StatusOK, &resp)
|
||||
}
|
||||
Reference in New Issue
Block a user