This is an automated email from the ASF dual-hosted git repository.
warren 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 8e5562f06 test(tapd): add unit test for shared (#4770)
8e5562f06 is described below
commit 8e5562f0663de06bb14d656e3d92d54208c03a45
Author: Warren Chen <[email protected]>
AuthorDate: Tue Mar 28 13:26:51 2023 +0800
test(tapd): add unit test for shared (#4770)
---
backend/plugins/tapd/tasks/shared.go | 56 +++++-
backend/plugins/tapd/tasks/shared_test.go | 301 ++++++++++++++++++++++++++++++
2 files changed, 350 insertions(+), 7 deletions(-)
diff --git a/backend/plugins/tapd/tasks/shared.go
b/backend/plugins/tapd/tasks/shared.go
index a0117a5fa..9a5726efb 100644
--- a/backend/plugins/tapd/tasks/shared.go
+++ b/backend/plugins/tapd/tasks/shared.go
@@ -96,9 +96,12 @@ func GetTotalPagesFromResponse(r *http.Response, args
*api.ApiCollectorArgs) (in
return totalPage, err
}
+// parseIterationChangelog function is used to parse the iteration changelog
func parseIterationChangelog(taskCtx plugin.SubTaskContext, old string, new
string) (iterationFromId uint64, iterationToId uint64, err errors.Error) {
data := taskCtx.GetData().(*TapdTaskData)
db := taskCtx.GetDal()
+
+ // Find the iteration with the old name
iterationFrom := &models.TapdIteration{}
clauses := []dal.Clause{
dal.From(&models.TapdIteration{}),
@@ -110,6 +113,7 @@ func parseIterationChangelog(taskCtx plugin.SubTaskContext,
old string, new stri
return 0, 0, err
}
+ // Find the iteration with the new name
iterationTo := &models.TapdIteration{}
clauses = []dal.Clause{
dal.From(&models.TapdIteration{}),
@@ -120,15 +124,19 @@ func parseIterationChangelog(taskCtx
plugin.SubTaskContext, old string, new stri
if err != nil && !db.IsErrorNotFound(err) {
return 0, 0, err
}
+
return iterationFrom.Id, iterationTo.Id, nil
}
+// GetRawMessageDirectFromResponse extracts the raw message from an HTTP
response
func GetRawMessageDirectFromResponse(res *http.Response) ([]json.RawMessage,
errors.Error) {
+ // Read the response body
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
return nil, errors.Convert(err)
}
+ // Return the response body as a slice of json.RawMessage
return []json.RawMessage{body}, nil
}
@@ -146,83 +154,111 @@ type TapdApiParams struct {
WorkspaceId uint64
}
+// CreateRawDataSubTaskArgs creates a new instance of api.RawDataSubTaskArgs
based on the provided
+// task context, raw table name, and a flag to determine if the company ID
should be used.
+// It returns a pointer to the created api.RawDataSubTaskArgs and a pointer to
the filtered TapdTaskData.
func CreateRawDataSubTaskArgs(taskCtx plugin.SubTaskContext, rawTable string,
useCompanyId bool) (*api.RawDataSubTaskArgs, *TapdTaskData) {
+ // Retrieve task data from the provided task context and cast it to
TapdTaskData
data := taskCtx.GetData().(*TapdTaskData)
+ // Create a filtered copy of the original data
filteredData := *data
filteredData.Options = &TapdOptions{}
*filteredData.Options = *data.Options
+ // Set up TapdApiParams based on the original data
var params = TapdApiParams{
ConnectionId: data.Options.ConnectionId,
WorkspaceId: data.Options.WorkspaceId,
}
+ // Check if the company ID should be used and if it is not zero, add it
to the params
if data.Options.CompanyId != 0 && useCompanyId {
params.CompanyId = data.Options.CompanyId
} else {
filteredData.Options.CompanyId = 0
}
+ // Create the RawDataSubTaskArgs with the task context, params, and raw
table name
rawDataSubTaskArgs := &api.RawDataSubTaskArgs{
Ctx: taskCtx,
Params: params,
Table: rawTable,
}
+ // Return the created RawDataSubTaskArgs and the filtered TapdTaskData
return rawDataSubTaskArgs, &filteredData
}
-// getTapdTypeMappings will map story types in _tool_tapd_workitem_types to
our typeMapping
+// getTapdTypeMappings retrieves story types from _tool_tapd_workitem_types
and maps them to
+// typeMapping. It takes TapdTaskData, a Dal interface, and a system string as
arguments.
+// It returns a map of type ID to type name and an error, if any.
func getTapdTypeMappings(data *TapdTaskData, db dal.Dal, system string)
(map[uint64]string, errors.Error) {
typeIdMapping := make(map[uint64]string)
issueTypes := make([]models.TapdWorkitemType, 0)
+ // Create clauses for querying the database
clauses := []dal.Clause{
dal.From(&models.TapdWorkitemType{}),
dal.Where("connection_id = ? and workspace_id = ? and
entity_type = ?",
data.Options.ConnectionId, data.Options.WorkspaceId,
system),
}
+ // Query the database for issue types
err := db.All(&issueTypes, clauses...)
if err != nil {
return nil, err
}
+ // Map the retrieved issue types
for _, issueType := range issueTypes {
typeIdMapping[issueType.Id] = issueType.Name
}
return typeIdMapping, nil
}
+// getStdTypeMappings creates a map of user type to standard type based on the
provided TapdTaskData.
+// It returns the created map.
func getStdTypeMappings(data *TapdTaskData) map[string]string {
stdTypeMappings := make(map[string]string)
+ // Map user types to standard types
for userType, stdType := range
data.Options.TransformationRules.TypeMappings {
stdTypeMappings[userType] =
strings.ToUpper(stdType.StandardType)
}
return stdTypeMappings
}
+// getStatusMapping creates a map of original status values to standard status
values
+// based on the provided TapdTaskData. It returns the created map.
func getStatusMapping(data *TapdTaskData) map[string]string {
statusMapping := make(map[string]string)
mapping := data.Options.TransformationRules.StatusMappings
+ // Map original status values to standard status values
for std, orig := range mapping {
for _, v := range orig {
statusMapping[v] = std
}
}
-
return statusMapping
}
+// getDefaultStdStatusMapping retrieves default standard status mappings for
the given TapdTaskData and status list.
+// It takes TapdTaskData, a Dal interface, and a statusList of type S
(models.TapdStatus).
+// It returns a map of English to Chinese status names, a function to get
standard status from status key, and an error, if any.
func getDefaultStdStatusMapping[S models.TapdStatus](data *TapdTaskData, db
dal.Dal, statusList []S) (map[string]string, func(statusKey string) string,
errors.Error) {
+ // Create clauses for querying the database
clauses := []dal.Clause{
dal.Where("connection_id = ? and workspace_id = ?",
data.Options.ConnectionId, data.Options.WorkspaceId),
}
+ // Query the database for status list
err := db.All(&statusList, clauses...)
if err != nil {
return nil, nil, err
}
+ // Create status language and last step maps
statusLanguageMap := make(map[string]string, len(statusList))
statusLastStepMap := make(map[string]bool, len(statusList))
+ // Populate status maps
for _, v := range statusList {
statusLanguageMap[v.GetEnglish()] = v.GetChinese()
statusLastStepMap[v.GetChinese()] = v.GetIsLastStep()
}
+
+ // Define function to get standard status from status key
getStdStatus := func(statusKey string) string {
if statusLastStepMap[statusKey] {
return ticket.DONE
@@ -235,8 +271,9 @@ func getDefaultStdStatusMapping[S models.TapdStatus](data
*TapdTaskData, db dal.
return statusLanguageMap, getStdStatus, nil
}
+// unicodeToZh converts a string containing Unicode escape sequences to a
Chinese string.
+// It returns the converted string and an error if the conversion fails.
func unicodeToZh(s string) (string, error) {
- // strconv.Quote(s) will add additional `"`, so we need to do `Unquote`
again
str, err := strconv.Unquote(strings.Replace(strconv.Quote(s), `\\u`,
`\u`, -1))
if err != nil {
return "", err
@@ -244,6 +281,8 @@ func unicodeToZh(s string) (string, error) {
return str, nil
}
+// convertUnicode converts the ValueAfterParsed and ValueBeforeParsed fields
of a struct to Chinese text.
+// It takes a pointer to a struct and returns an error if the conversion fails.
func convertUnicode(p interface{}) errors.Error {
var err errors.Error
pType := reflect.TypeOf(p)
@@ -268,7 +307,7 @@ func convertUnicode(p interface{}) errors.Error {
if before == "--" {
before = ""
}
- // set ValueAfterParsed
+ // Set ValueAfterParsed and ValueBeforeParsed fields
valueAfterField := pValue.FieldByName("ValueAfterParsed")
valueAfterField.SetString(after)
valueBeforeField := pValue.FieldByName("ValueBeforeParsed")
@@ -276,14 +315,17 @@ func convertUnicode(p interface{}) errors.Error {
return nil
}
-// replace ";" with ","
+// replaceSemicolonWithComma replaces all semicolons with commas in the given
string
+// and trims any trailing commas. It returns the modified string.
func replaceSemicolonWithComma(str string) string {
res := strings.ReplaceAll(str, ";", ",")
return strings.TrimRight(res, ",")
}
-// generate domain account id for each user
-// param is a string with format "user1,user2,user3"
+// generateDomainAccountIdForUsers generates domain account IDs for a list of
users.
+// The input 'param' is a string with format "user1,user2,user3".
+// The function takes a string containing a list of users separated by commas
and a connectionId.
+// It returns a string containing the generated domain account IDs for each
user, separated by commas.
func generateDomainAccountIdForUsers(param string, connectionId uint64) string
{
if param == "" {
return ""
diff --git a/backend/plugins/tapd/tasks/shared_test.go
b/backend/plugins/tapd/tasks/shared_test.go
new file mode 100644
index 000000000..9569704a8
--- /dev/null
+++ b/backend/plugins/tapd/tasks/shared_test.go
@@ -0,0 +1,301 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 tasks
+
+import (
+ "encoding/json"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
+ mockplugin "github.com/apache/incubator-devlake/mocks/core/plugin"
+ "github.com/apache/incubator-devlake/plugins/tapd/models"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "io/ioutil"
+ "net/http"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// TestParseIterationChangelog tests the parseIterationChangelog function
+func TestParseIterationChangelog(t *testing.T) {
+ data := &TapdTaskData{Options: &TapdOptions{CompanyId: 1, WorkspaceId:
1, ConnectionId: 1}}
+
+ // Set up the required data for testing
+ mockCtx := new(mockplugin.SubTaskContext)
+ mockDal := new(mockdal.Dal)
+
+ mockCtx.On("GetData").Return(data)
+ mockCtx.On("GetDal").Return(mockDal)
+ // Set up the required data for testing
+ mockIterationFrom := &models.TapdIteration{
+ ConnectionId: 1,
+ Id: 1,
+ Name: "",
+ }
+ mockIterationTo := &models.TapdIteration{
+ ConnectionId: 1,
+ Id: 2,
+ Name: "",
+ }
+ mockDal.On("First", mock.Anything, mock.Anything).Run(func(args
mock.Arguments) {
+ dst := args.Get(0).(*models.TapdIteration)
+ *dst = *mockIterationFrom
+ }).Return(nil).Once()
+ mockDal.On("First", mock.Anything, mock.Anything).Run(func(args
mock.Arguments) {
+ dst := args.Get(0).(*models.TapdIteration)
+ *dst = *mockIterationTo
+ }).Return(nil).Once()
+ // Test case 2: success scenario
+ iterationFromId, iterationToId, err := parseIterationChangelog(mockCtx,
"old", "new")
+ assert.Equal(t, uint64(1), iterationFromId)
+ assert.Equal(t, uint64(2), iterationToId)
+ assert.Nil(t, err)
+}
+
+func TestGetRawMessageDirectFromResponse(t *testing.T) {
+ // Create a mock HTTP response
+ body := `{"data": {"count": 10}}`
+ res := &http.Response{
+ StatusCode: http.StatusOK,
+ Body: ioutil.NopCloser(strings.NewReader(body)),
+ }
+
+ // Call the function and check the result
+ rawMessages, err := GetRawMessageDirectFromResponse(res)
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ if len(rawMessages) != 1 {
+ t.Errorf("Expected 1 raw message, got %d", len(rawMessages))
+ }
+ var page Page
+ err = errors.Convert(json.Unmarshal(rawMessages[0], &page))
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+ if page.Data.Count != 10 {
+ t.Errorf("Expected count to be 10, got %d", page.Data.Count)
+ }
+}
+
+func TestGetTapdTypeMappings(t *testing.T) {
+ // create a mock database connection
+ db := new(mockdal.Dal)
+ // create some test data
+ data := &TapdTaskData{
+ Options: &TapdOptions{
+ ConnectionId: 1,
+ WorkspaceId: 2,
+ },
+ }
+
+ issueTypes := make([]models.TapdWorkitemType, 0)
+ issueTypes = append(issueTypes, models.TapdWorkitemType{
+ ConnectionId: 1,
+ WorkspaceId: 2,
+ Id: 1,
+ Name: "Story",
+ })
+ issueTypes = append(issueTypes, models.TapdWorkitemType{
+ ConnectionId: 1,
+ WorkspaceId: 2,
+ Id: 2,
+ Name: "Bug",
+ })
+ db.On("All", mock.Anything, mock.Anything).Run(func(args
mock.Arguments) {
+ dst := args.Get(0).(*[]models.TapdWorkitemType)
+ *dst = issueTypes
+ }).Return(nil).Once()
+ // call the function being tested
+ result, err := getTapdTypeMappings(data, db, "story")
+
+ // check if the result is correct
+ if err != nil {
+ t.Errorf("getTapdTypeMappings returned an error: %v", err)
+ }
+ if len(result) != 2 {
+ t.Errorf("getTapdTypeMappings returned %d items, expected 2",
len(result))
+ }
+ if result[1] != "Story" {
+ t.Errorf("getTapdTypeMappings returned incorrect value for ID
1: %s", result[1])
+ }
+ if result[2] != "Bug" {
+ t.Errorf("getTapdTypeMappings returned incorrect value for ID
2: %s", result[2])
+ }
+}
+
+func TestGetStdTypeMappings(t *testing.T) {
+ data := &TapdTaskData{
+ Options: &TapdOptions{
+ TransformationRules: TransformationRules{
+ TypeMappings: map[string]TypeMapping{
+ "userType": {
+ StandardType: "stdType",
+ },
+ },
+ },
+ },
+ }
+ expected := map[string]string{
+ "userType": "STDTYPE",
+ }
+ result := getStdTypeMappings(data)
+ if !reflect.DeepEqual(result, expected) {
+ t.Errorf("Expected %v but got %v", expected, result)
+ }
+}
+
+func TestGetStatusMapping(t *testing.T) {
+ data := &TapdTaskData{
+ Options: &TapdOptions{
+ TransformationRules: TransformationRules{
+ StatusMappings: StatusMappings{
+ "Done": {"已完成", "已关闭"},
+ "In Progress": {"进行中", "处理中"},
+ "To Do": {"未开始", "草稿"},
+ },
+ },
+ },
+ }
+
+ expected := map[string]string{
+ "已完成": "Done",
+ "已关闭": "Done",
+ "进行中": "In Progress",
+ "处理中": "In Progress",
+ "未开始": "To Do",
+ "草稿": "To Do",
+ }
+
+ result := getStatusMapping(data)
+
+ if !reflect.DeepEqual(result, expected) {
+ t.Errorf("Expected %v, but got %v", expected, result)
+ }
+}
+
+// test case for when the status list is empty
+func TestGetDefaultStdStatusMappingEmptyStatusList(t *testing.T) {
+ data := &TapdTaskData{
+ Options: &TapdOptions{
+ ConnectionId: 123,
+ WorkspaceId: 456,
+ },
+ }
+ db := new(mockdal.Dal)
+ statusList := []models.TapdStoryStatus{
+ {
+ ConnectionId: 123,
+ WorkspaceId: 456,
+ EnglishName: "Done",
+ ChineseName: "已完成",
+ IsLastStep: true,
+ },
+ {
+ ConnectionId: 123,
+ WorkspaceId: 456,
+ EnglishName: "In Progress",
+ ChineseName: "进行中",
+ },
+ }
+ db.On("All", mock.Anything, mock.Anything).Run(func(args
mock.Arguments) {
+ dst := args.Get(0).(*[]models.TapdStoryStatus)
+ *dst = statusList
+ }).Return(nil).Once()
+ statusLanguageMap, getStdStatus, err :=
getDefaultStdStatusMapping(data, db, statusList)
+ if err != nil {
+ t.Errorf("getDefaultStdStatusMapping returned an error: %v",
err)
+ }
+ expectedStatusLanguageMap := map[string]string{
+ "Done": "已完成",
+ "In Progress": "进行中",
+ }
+ if !reflect.DeepEqual(statusLanguageMap, expectedStatusLanguageMap) {
+ t.Errorf("getDefaultStdStatusMapping returned unexpected
statusLanguageMap: got %v, want %v", statusLanguageMap,
expectedStatusLanguageMap)
+ }
+ expectedGetStdStatus := map[string]string{
+ "已完成": "DONE",
+ "进行中": "IN_PROGRESS",
+ }
+ for k, v := range expectedGetStdStatus {
+ if getStdStatus(k) != v {
+ t.Errorf("getDefaultStdStatusMapping returned
unexpected getStdStatus for %v: got %v, want %v", k, getStdStatus(k), v)
+ }
+ }
+}
+
+func TestUnicodeToZh(t *testing.T) {
+ input := "\\u4e2d\\u6587"
+ expected := "中文"
+ output, err := unicodeToZh(input)
+ if err != nil {
+ t.Errorf("unicodeToZh(%q) returned error %v", input, err)
+ }
+ if output != expected {
+ t.Errorf("unicodeToZh(%q) = %q, want %q", input, output,
expected)
+ }
+}
+
+func TestConvertUnicode(t *testing.T) {
+ testStruct := struct {
+ ValueBeforeParsed string
+ ValueAfterParsed string
+ }{
+ ValueBeforeParsed: "Hello, \\u4e16\\u754c!",
+ ValueAfterParsed: "--",
+ }
+
+ err := convertUnicode(&testStruct)
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+
+ expectedBefore := "Hello, 世界!"
+ if testStruct.ValueBeforeParsed != expectedBefore {
+ t.Errorf("Expected ValueBeforeParsed to be %q, but got %q",
expectedBefore, testStruct.ValueBeforeParsed)
+ }
+
+ expectedAfter := ""
+ if testStruct.ValueAfterParsed != expectedAfter {
+ t.Errorf("Expected ValueAfterParsed to be %q, but got %q",
expectedAfter, testStruct.ValueAfterParsed)
+ }
+}
+
+func TestGenerateDomainAccountIdForUsers(t *testing.T) {
+ connectionId := uint64(123)
+ testCases := []struct {
+ param string
+ expected string
+ }{
+ {"user1,user2,user3",
"tapd:TapdAccount:123:user1,tapd:TapdAccount:123:user2,tapd:TapdAccount:123:user3"},
+ {"user4;user5;user6",
"tapd:TapdAccount:123:user4,tapd:TapdAccount:123:user5,tapd:TapdAccount:123:user6"},
+ {"", ""},
+ }
+ mockMeta := mockplugin.NewPluginMeta(t)
+
mockMeta.On("RootPkgPath").Return("github.com/apache/incubator-devlake/plugins/tapd")
+ err := plugin.RegisterPlugin("tapd", mockMeta)
+ assert.Nil(t, err)
+ for _, testCase := range testCases {
+ result := generateDomainAccountIdForUsers(testCase.param,
connectionId)
+ if result != testCase.expected {
+ t.Errorf("generateDomainAccountIdForUsers(%s, %d) = %s;
expected %s", testCase.param, connectionId, result, testCase.expected)
+ }
+ }
+}