You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2022/06/20 14:34:34 UTC

[incubator-devlake] branch main updated: feat: multi-data connections support for ae (#2208)

This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 7b64c9e8 feat: multi-data connections support for ae (#2208)
7b64c9e8 is described below

commit 7b64c9e8f7643d8c65a2bcfb497eedb9fc2e761e
Author: mappjzc <zh...@merico.dev>
AuthorDate: Mon Jun 20 22:34:28 2022 +0800

    feat: multi-data connections support for ae (#2208)
    
    * feat: multi-data connections support for ae
    
    Add multi-data support for AE
    Mv randomstr to core
    Add TestConnection for AE
    
    Nddtfjiang <zh...@merico.dev>
    
    * refactor: change test api to projects
    
    Change test api to projects from projects/
    Move GetSign from api to models.
    Remove Tasks of AeOptions.
    
    Nddtfjiang <zh...@merico.dev>
    
    * refactor: spread out aeconnection at migrationscripts
    
    spread out aeconnection at migrationscripts
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix services init early than main
    
    Change services init to Init and call it on main
    
    Nddtfjiang <zh...@merico.dev>
    
    * refactor: change connction test from param not db
    
    Change connction test to use param not db.
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix e2e test lost init
    
    Add init to e2e test TestNewTask
    
    Nddtfjiang <zh...@merico.dev>
---
 main.go                                            |   2 +
 plugins/ae/ae.go                                   |  38 +++++-
 plugins/ae/api/connection.go                       | 135 ++++++++++++++-------
 plugins/ae/models/connection.go                    |  54 ++++++++-
 .../{ => migrationscripts/archived}/connection.go  |  29 +++--
 .../migrationscripts/updateSchemas20220615.go}     |  28 +++--
 plugins/ae/tasks/api_client.go                     |  71 ++---------
 plugins/ae/tasks/task_data.go                      |   4 +-
 plugins/core/plugin_utils.go                       |  14 +++
 plugins/helper/connection.go                       |   5 +
 services/init.go                                   |   2 +-
 test/api/task/task_test.go                         |  27 +++--
 12 files changed, 263 insertions(+), 146 deletions(-)

diff --git a/main.go b/main.go
index 7c3001f7..2ffe728d 100644
--- a/main.go
+++ b/main.go
@@ -21,6 +21,7 @@ import (
 	"github.com/apache/incubator-devlake/api"
 	"github.com/apache/incubator-devlake/config"
 	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/services"
 	_ "github.com/apache/incubator-devlake/version"
 )
 
@@ -36,5 +37,6 @@ func main() {
 			panic(err)
 		}
 	}
+	services.Init()
 	api.CreateApiService()
 }
diff --git a/plugins/ae/ae.go b/plugins/ae/ae.go
index e2bad02f..4ca20b24 100644
--- a/plugins/ae/ae.go
+++ b/plugins/ae/ae.go
@@ -22,9 +22,11 @@ import (
 
 	"github.com/apache/incubator-devlake/migration"
 	"github.com/apache/incubator-devlake/plugins/ae/api"
+	"github.com/apache/incubator-devlake/plugins/ae/models"
 	"github.com/apache/incubator-devlake/plugins/ae/models/migrationscripts"
 	"github.com/apache/incubator-devlake/plugins/ae/tasks"
 	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
 	"github.com/apache/incubator-devlake/runner"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cobra"
@@ -41,6 +43,7 @@ var _ core.Migratable = (*AE)(nil)
 type AE struct{}
 
 func (plugin AE) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) error {
+	api.Init(config, logger, db)
 	return nil
 }
 
@@ -67,10 +70,26 @@ func (plugin AE) PrepareTaskData(taskCtx core.TaskContext, options map[string]in
 	if op.ProjectId <= 0 {
 		return nil, fmt.Errorf("projectId is required")
 	}
-	apiClient, err := tasks.CreateApiClient(taskCtx)
+
+	connection := &models.AeConnection{}
+	connectionHelper := helper.NewConnectionHelper(
+		taskCtx,
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	err = connectionHelper.FirstById(connection, op.ConnectionId)
 	if err != nil {
 		return nil, err
 	}
+
+	apiClient, err := tasks.CreateApiClient(taskCtx, connection)
+	if err != nil {
+		return nil, err
+	}
+
 	return &tasks.AeTaskData{
 		Options:   &op,
 		ApiClient: apiClient,
@@ -82,7 +101,10 @@ func (plugin AE) RootPkgPath() string {
 }
 
 func (plugin AE) MigrationScripts() []migration.Script {
-	return []migration.Script{new(migrationscripts.InitSchemas)}
+	return []migration.Script{
+		new(migrationscripts.InitSchemas),
+		new(migrationscripts.UpdateSchemas20220615),
+	}
 }
 
 func (plugin AE) ApiResources() map[string]map[string]core.ApiResourceHandler {
@@ -91,11 +113,13 @@ func (plugin AE) ApiResources() map[string]map[string]core.ApiResourceHandler {
 			"GET": api.TestConnection,
 		},
 		"connections": {
-			"GET": api.ListConnections,
+			"GET":  api.ListConnections,
+			"POST": api.PostConnections,
 		},
 		"connections/:connectionId": {
-			"GET":   api.GetConnection,
-			"PATCH": api.PatchConnection,
+			"GET":    api.GetConnection,
+			"PATCH":  api.PatchConnection,
+			"DELETE": api.DeleteConnection,
 		},
 	}
 }
@@ -105,11 +129,13 @@ var PluginEntry AE //nolint
 
 func main() {
 	aeCmd := &cobra.Command{Use: "ae"}
+	connectionId := aeCmd.Flags().Uint64P("Connection-id", "c", 0, "ae connection id")
 	projectId := aeCmd.Flags().IntP("project-id", "p", 0, "ae project id")
 	_ = aeCmd.MarkFlagRequired("project-id")
 	aeCmd.Run = func(cmd *cobra.Command, args []string) {
 		runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
-			"projectId": *projectId,
+			"connectionId": *connectionId,
+			"projectId":    *projectId,
 		})
 	}
 	runner.RunCmd(aeCmd)
diff --git a/plugins/ae/api/connection.go b/plugins/ae/api/connection.go
index c66f9e11..9dc2119b 100644
--- a/plugins/ae/api/connection.go
+++ b/plugins/ae/api/connection.go
@@ -18,88 +18,141 @@ limitations under the License.
 package api
 
 import (
-	"github.com/apache/incubator-devlake/config"
+	"fmt"
+	"net/http"
+	"time"
+
 	"github.com/apache/incubator-devlake/plugins/ae/models"
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
-	"net/http"
+	"github.com/go-playground/validator/v10"
+	"github.com/mitchellh/mapstructure"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
 )
 
 type ApiMeResponse struct {
 	Name string `json:"name"`
 }
 
-/*
-GET /plugins/ae/test
-*/
-func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// TODO: implement test connection
-	return &core.ApiResourceOutput{Body: true}, nil
+var vld *validator.Validate
+var connectionHelper *helper.ConnectionApiHelper
+
+func Init(config *viper.Viper, logger core.Logger, database *gorm.DB) {
+	basicRes := helper.NewDefaultBasicRes(config, logger, database)
+	vld = validator.New()
+	connectionHelper = helper.NewConnectionHelper(
+		basicRes,
+		vld,
+	)
 }
 
 /*
-PATCH /plugins/ae/connections/:connectionId
+GET /plugins/ae/test/
 */
-func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	v := config.GetConfig()
-	connection := &models.AeConnection{}
-	err := helper.EncodeStruct(v, connection, "env")
+func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	// decode
+	var err error
+	var connection models.TestConnectionRequest
+	err = mapstructure.Decode(input.Body, &connection)
 	if err != nil {
 		return nil, err
 	}
-	// update from request and save to .env
-	err = helper.DecodeStruct(v, connection, input.Body, "env")
+	// validate
+	err = vld.Struct(connection)
 	if err != nil {
 		return nil, err
 	}
-	err = config.WriteConfig(v)
+
+	// load and process cconfiguration
+	endpoint := connection.Endpoint
+	appId := connection.AppId
+	secretKey := connection.SecretKey
+	proxy := connection.Proxy
+
+	apiClient, err := helper.NewApiClient(endpoint, nil, 3*time.Second, proxy, nil)
+	if err != nil {
+		return nil, err
+	}
+	apiClient.SetBeforeFunction(func(req *http.Request) error {
+		nonceStr := core.RandLetterBytes(8)
+		timestamp := fmt.Sprintf("%v", time.Now().Unix())
+		sign := models.GetSign(req.URL.Query(), appId, secretKey, nonceStr, timestamp)
+		req.Header.Set("x-ae-app-id", appId)
+		req.Header.Set("x-ae-timestamp", timestamp)
+		req.Header.Set("x-ae-nonce-str", nonceStr)
+		req.Header.Set("x-ae-sign", sign)
+		return nil
+	})
+	res, err := apiClient.Get("projects", nil, nil)
 	if err != nil {
 		return nil, err
 	}
-	response := models.AeResponse{
-		AeConnection: *connection,
-		Name:         "Ae",
-		ID:           1,
+
+	switch res.StatusCode {
+	case 200: // right StatusCode
+		return &core.ApiResourceOutput{Body: true, Status: 200}, nil
+	case 401: // error secretKey or nonceStr
+		return &core.ApiResourceOutput{Body: false, Status: res.StatusCode}, nil
+	default: // unknow what happen , back to user
+		return &core.ApiResourceOutput{Body: res.Body, Status: res.StatusCode}, nil
 	}
-	return &core.ApiResourceOutput{Body: response, Status: http.StatusOK}, nil
 }
 
 /*
-GET /plugins/ae/connections
+POST /plugins/ae/connections
 */
-func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// RETURN ONLY 1 SOURCE (FROM ENV) until multi-connection is developed.
-	v := config.GetConfig()
+func PostConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
 	connection := &models.AeConnection{}
-
-	err := helper.EncodeStruct(v, connection, "env")
+	err := connectionHelper.Create(connection, input)
 	if err != nil {
 		return nil, err
 	}
-	response := models.AeResponse{
-		AeConnection: *connection,
-		Name:         "Ae",
-		ID:           1,
-	}
+	return &core.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
 
-	return &core.ApiResourceOutput{Body: []models.AeResponse{response}}, nil
+/*
+GET /plugins/ae/connections
+*/
+func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	var connections []models.AeConnection
+	err := connectionHelper.List(&connections)
+	if err != nil {
+		return nil, err
+	}
+	return &core.ApiResourceOutput{Body: connections, Status: http.StatusOK}, nil
 }
 
 /*
 GET /plugins/ae/connections/:connectionId
 */
 func GetConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	//  RETURN ONLY 1 SOURCE FROM ENV (Ignore ID until multi-connection is developed.)
-	v := config.GetConfig()
 	connection := &models.AeConnection{}
-	err := helper.EncodeStruct(v, connection, "env")
+	err := connectionHelper.First(connection, input.Params)
+	return &core.ApiResourceOutput{Body: connection}, err
+}
+
+/*
+PATCH /plugins/ae/connections/:connectionId
+*/
+func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	connection := &models.AeConnection{}
+	err := connectionHelper.Patch(connection, input)
 	if err != nil {
 		return nil, err
 	}
-	response := &models.AeResponse{
-		AeConnection: *connection,
-		Name:         "Ae",
-		ID:           1,
+	return &core.ApiResourceOutput{Body: connection, Status: http.StatusOK}, nil
+}
+
+/*
+DELETE /plugins/ae/connections/:connectionId
+*/
+func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	connection := &models.AeConnection{}
+	err := connectionHelper.First(connection, input.Params)
+	if err != nil {
+		return nil, err
 	}
-	return &core.ApiResourceOutput{Body: response}, nil
+	err = connectionHelper.Delete(connection)
+	return &core.ApiResourceOutput{Body: connection}, err
 }
diff --git a/plugins/ae/models/connection.go b/plugins/ae/models/connection.go
index 99624b56..96ae9b1c 100644
--- a/plugins/ae/models/connection.go
+++ b/plugins/ae/models/connection.go
@@ -17,11 +17,26 @@ limitations under the License.
 
 package models
 
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"net/url"
+	"sort"
+	"strings"
+
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
 type AeConnection struct {
-	AppId    string `mapstructure:"appId" env:"AE_APP_ID" json:"appId"`
-	Sign     string `mapstructure:"sign" env:"AE_SIGN" json:"sign"`
-	NonceStr string `mapstructure:"nonceStr" env:"AE_NONCE_STR" json:"nonceStr"`
-	Endpoint string `mapstructure:"endpoint" env:"AE_ENDPOINT" json:"endpoint"`
+	helper.RestConnection `mapstructure:",squash"`
+	helper.AppKey         `mapstructure:",squash"`
+}
+
+type TestConnectionRequest struct {
+	Endpoint      string `json:"endpoint"`
+	Proxy         string `json:"proxy"`
+	helper.AppKey `mapstructure:",squash"`
 }
 
 // This object conforms to what the frontend currently expects.
@@ -30,3 +45,34 @@ type AeResponse struct {
 	Name string `json:"name"`
 	ID   int    `json:"id"`
 }
+
+func (AeConnection) TableName() string {
+	return "_tool_ae_connections"
+}
+
+func GetSign(query url.Values, appId, secretKey, nonceStr, timestamp string) string {
+	// clone query because we need to add items
+	kvs := make([]string, 0, len(query)+3)
+	kvs = append(kvs, fmt.Sprintf("app_id=%s", appId))
+	kvs = append(kvs, fmt.Sprintf("timestamp=%s", timestamp))
+	kvs = append(kvs, fmt.Sprintf("nonce_str=%s", nonceStr))
+	for key, values := range query {
+		for _, value := range values {
+			kvs = append(kvs, fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(value)))
+		}
+	}
+
+	// sort by alphabetical order
+	sort.Strings(kvs)
+
+	// generate text for signature
+	querystring := fmt.Sprintf("%s&key=%s", strings.Join(kvs, "&"), url.QueryEscape(secretKey))
+
+	// sign it
+	hasher := md5.New()
+	_, err := hasher.Write([]byte(querystring))
+	if err != nil {
+		return ""
+	}
+	return strings.ToUpper(hex.EncodeToString(hasher.Sum(nil)))
+}
diff --git a/plugins/ae/models/connection.go b/plugins/ae/models/migrationscripts/archived/connection.go
similarity index 52%
copy from plugins/ae/models/connection.go
copy to plugins/ae/models/migrationscripts/archived/connection.go
index 99624b56..6cbfe39e 100644
--- a/plugins/ae/models/connection.go
+++ b/plugins/ae/models/migrationscripts/archived/connection.go
@@ -15,18 +15,27 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package models
+package archived
+
+import (
+	"time"
+)
 
 type AeConnection struct {
-	AppId    string `mapstructure:"appId" env:"AE_APP_ID" json:"appId"`
-	Sign     string `mapstructure:"sign" env:"AE_SIGN" json:"sign"`
-	NonceStr string `mapstructure:"nonceStr" env:"AE_NONCE_STR" json:"nonceStr"`
-	Endpoint string `mapstructure:"endpoint" env:"AE_ENDPOINT" json:"endpoint"`
+	Name string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+
+	ID        uint64    `gorm:"primaryKey" json:"id"`
+	CreatedAt time.Time `json:"createdAt"`
+	UpdatedAt time.Time `json:"updatedAt"`
+
+	Endpoint  string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
+	Proxy     string `mapstructure:"proxy" json:"proxy"`
+	RateLimit int    `comment:"api request rate limit per hour" json:"rateLimit"`
+
+	AppId     string `mapstructure:"app_id" validate:"required" json:"app_id"`
+	SecretKey string `mapstructure:"secret_key" validate:"required" json:"secret_key" encrypt:"yes"`
 }
 
-// This object conforms to what the frontend currently expects.
-type AeResponse struct {
-	AeConnection
-	Name string `json:"name"`
-	ID   int    `json:"id"`
+func (AeConnection) TableName() string {
+	return "_tool_ae_connections"
 }
diff --git a/plugins/ae/tasks/task_data.go b/plugins/ae/models/migrationscripts/updateSchemas20220615.go
similarity index 60%
copy from plugins/ae/tasks/task_data.go
copy to plugins/ae/models/migrationscripts/updateSchemas20220615.go
index 1ea21557..07c604a0 100644
--- a/plugins/ae/tasks/task_data.go
+++ b/plugins/ae/models/migrationscripts/updateSchemas20220615.go
@@ -15,19 +15,27 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package tasks
+package migrationscripts
 
-import "github.com/apache/incubator-devlake/plugins/helper"
+import (
+	"context"
 
-type AeOptions struct {
-	ProjectId int
-	Tasks     []string `json:"tasks,omitempty"`
+	"github.com/apache/incubator-devlake/plugins/ae/models/migrationscripts/archived"
+	"gorm.io/gorm"
+)
+
+type UpdateSchemas20220615 struct{}
+
+func (*UpdateSchemas20220615) Up(ctx context.Context, db *gorm.DB) error {
+	return db.Migrator().AutoMigrate(
+		&archived.AeConnection{},
+	)
 }
 
-type AeTaskData struct {
-	Options   *AeOptions
-	ApiClient *helper.ApiAsyncClient
+func (*UpdateSchemas20220615) Version() uint64 {
+	return 20220615181010
 }
-type AeApiParams struct {
-	ProjectId int
+
+func (*UpdateSchemas20220615) Name() string {
+	return "create tables:" + archived.AeConnection{}.TableName()
 }
diff --git a/plugins/ae/tasks/api_client.go b/plugins/ae/tasks/api_client.go
index da38e2c1..4ce1642e 100644
--- a/plugins/ae/tasks/api_client.go
+++ b/plugins/ae/tasks/api_client.go
@@ -18,85 +18,30 @@ limitations under the License.
 package tasks
 
 import (
-	"crypto/md5"
-	"encoding/hex"
 	"fmt"
-	"math/rand"
 	"net/http"
-	"net/url"
-	"sort"
-	"strings"
 	"time"
 
+	"github.com/apache/incubator-devlake/plugins/ae/models"
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 )
 
-const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-
-func init() {
-	rand.Seed(time.Now().UnixNano())
-}
-
-func RandString(n int) string {
-	b := make([]byte, n)
-	for i := range b {
-		b[i] = letterBytes[rand.Intn(len(letterBytes))]
-	}
-	return string(b)
-}
-
-func getSign(query url.Values, appId, secretKey, nonceStr, timestamp string) string {
-	// clone query because we need to add items
-	kvs := make([]string, 0, len(query)+3)
-	kvs = append(kvs, fmt.Sprintf("app_id=%s", appId))
-	kvs = append(kvs, fmt.Sprintf("timestamp=%s", timestamp))
-	kvs = append(kvs, fmt.Sprintf("nonce_str=%s", nonceStr))
-	for key, values := range query {
-		for _, value := range values {
-			kvs = append(kvs, fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(value)))
-		}
-	}
-
-	// sort by alphabetical order
-	sort.Strings(kvs)
-
-	// generate text for signature
-	querystring := fmt.Sprintf("%s&key=%s", strings.Join(kvs, "&"), url.QueryEscape(secretKey))
-
-	// sign it
-	hasher := md5.New()
-	_, err := hasher.Write([]byte(querystring))
-	if err != nil {
-		return ""
-	}
-	return strings.ToUpper(hex.EncodeToString(hasher.Sum(nil)))
-}
-
-func CreateApiClient(taskCtx core.TaskContext) (*helper.ApiAsyncClient, error) {
+func CreateApiClient(taskCtx core.TaskContext, connection *models.AeConnection) (*helper.ApiAsyncClient, error) {
 	// load and process cconfiguration
-	endpoint := taskCtx.GetConfig("AE_ENDPOINT")
-	if endpoint == "" {
-		return nil, fmt.Errorf("invalid AE_ENDPOINT")
-	}
-	appId := taskCtx.GetConfig("AE_APP_ID")
-	if appId == "" {
-		return nil, fmt.Errorf("invalid AE_APP_ID")
-	}
-	secretKey := taskCtx.GetConfig("AE_SECRET_KEY")
-	if secretKey == "" {
-		return nil, fmt.Errorf("invalid AE_SECRET_KEY")
-	}
-	proxy := taskCtx.GetConfig("AE_PROXY")
+	endpoint := connection.Endpoint
+	appId := connection.AppId
+	secretKey := connection.SecretKey
+	proxy := connection.Proxy
 
 	apiClient, err := helper.NewApiClient(endpoint, nil, 0, proxy, taskCtx.GetContext())
 	if err != nil {
 		return nil, err
 	}
 	apiClient.SetBeforeFunction(func(req *http.Request) error {
-		nonceStr := RandString(8)
+		nonceStr := core.RandLetterBytes(8)
 		timestamp := fmt.Sprintf("%v", time.Now().Unix())
-		sign := getSign(req.URL.Query(), appId, secretKey, nonceStr, timestamp)
+		sign := models.GetSign(req.URL.Query(), appId, secretKey, nonceStr, timestamp)
 		req.Header.Set("x-ae-app-id", appId)
 		req.Header.Set("x-ae-timestamp", timestamp)
 		req.Header.Set("x-ae-nonce-str", nonceStr)
diff --git a/plugins/ae/tasks/task_data.go b/plugins/ae/tasks/task_data.go
index 1ea21557..0bff2af5 100644
--- a/plugins/ae/tasks/task_data.go
+++ b/plugins/ae/tasks/task_data.go
@@ -20,8 +20,8 @@ package tasks
 import "github.com/apache/incubator-devlake/plugins/helper"
 
 type AeOptions struct {
-	ProjectId int
-	Tasks     []string `json:"tasks,omitempty"`
+	ConnectionId uint64 `json:"connectionId"`
+	ProjectId    int
 }
 
 type AeTaskData struct {
diff --git a/plugins/core/plugin_utils.go b/plugins/core/plugin_utils.go
index 95df35b6..b3f64380 100644
--- a/plugins/core/plugin_utils.go
+++ b/plugins/core/plugin_utils.go
@@ -30,6 +30,10 @@ import (
 
 const EncodeKeyEnvStr = "ENCODE_KEY"
 
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
+
 // TODO: maybe move encryption/decryption into helper?
 // AES + Base64 encryption using ENCODE_KEY in .env as key
 func Encrypt(encKey, plainText string) (string, error) {
@@ -152,3 +156,13 @@ func RandomCapsStr(len int) string {
 func RandomEncKey() string {
 	return RandomCapsStr(128)
 }
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+func RandLetterBytes(n int) string {
+	b := make([]byte, n)
+	for i := range b {
+		b[i] = letterBytes[rand.Intn(len(letterBytes))]
+	}
+	return string(b)
+}
diff --git a/plugins/helper/connection.go b/plugins/helper/connection.go
index f0c3f8d8..14d66a65 100644
--- a/plugins/helper/connection.go
+++ b/plugins/helper/connection.go
@@ -49,6 +49,11 @@ type AccessToken struct {
 	Token string `mapstructure:"token" validate:"required" json:"token" encrypt:"yes"`
 }
 
+type AppKey struct {
+	AppId     string `mapstructure:"app_id" validate:"required" json:"app_id"`
+	SecretKey string `mapstructure:"secret_key" validate:"required" json:"secret_key" encrypt:"yes"`
+}
+
 type RestConnection struct {
 	BaseConnection `mapstructure:",squash"`
 	Endpoint       string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
diff --git a/services/init.go b/services/init.go
index dd70c17b..63a69292 100644
--- a/services/init.go
+++ b/services/init.go
@@ -39,7 +39,7 @@ var db *gorm.DB
 var cronManager *cron.Cron
 var log core.Logger
 
-func init() {
+func Init() {
 	var err error
 	cfg = config.GetConfig()
 	log = logger.Global
diff --git a/test/api/task/task_test.go b/test/api/task/task_test.go
index 54d88471..1cc712d4 100644
--- a/test/api/task/task_test.go
+++ b/test/api/task/task_test.go
@@ -25,24 +25,33 @@ import (
 	"testing"
 
 	"github.com/apache/incubator-devlake/api"
+	"github.com/apache/incubator-devlake/config"
 	"github.com/apache/incubator-devlake/models"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/services"
 	"github.com/gin-gonic/gin"
 	"github.com/magiconair/properties/assert"
-	"github.com/stretchr/testify/mock"
 )
 
+func init() {
+	v := config.GetConfig()
+	encKey := v.GetString(core.EncodeKeyEnvStr)
+	if encKey == "" {
+		// Randomly generate a bunch of encryption keys and set them to config
+		encKey = core.RandomEncKey()
+		v.Set(core.EncodeKeyEnvStr, encKey)
+		err := config.WriteConfig(v)
+		if err != nil {
+			panic(err)
+		}
+	}
+	services.Init()
+}
+
 func TestNewTask(t *testing.T) {
 	r := gin.Default()
 	api.RegisterRouter(r)
 
-	type services struct {
-		mock.Mock
-	}
-
-	// fakeTask := models.Task{}
-	testObj := new(services)
-	testObj.On("CreateTask").Return(true, nil)
-
 	w := httptest.NewRecorder()
 	params := strings.NewReader(`{"name": "hello", "tasks": [[{ "plugin": "jira", "options": { "host": "www.jira.com" } }]]}`)
 	req, _ := http.NewRequest("POST", "/pipelines", params)