Skip to content

Commit f03c7c9

Browse files
check if link in tmetric is a valid OP link (#15)
Co-authored-by: Sawjan Gurung <saw.jan.grg3e@gmail.com>
1 parent c4630cb commit f03c7c9

File tree

6 files changed

+119
-19
lines changed

6 files changed

+119
-19
lines changed

cmd/copy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func checkTmetricEntries(tmetricUser tmetric.User, config *config.Config) ([]tme
4444
)
4545
}
4646

47-
if len(tmetric.GetEntriesWithoutLinkToOpenProject(timeEntries)) > 0 {
47+
if len(tmetric.GetEntriesWithoutLinkToOpenProject(config, timeEntries)) > 0 {
4848
return nil, fmt.Errorf(
4949
"some time-entries are not linked to an OpenProject work-package, run the 'check tmetric' command to fix it",
5050
)

cmd/tmetric.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,15 @@ func validateOpenProjectWorkPackage(input string) error {
4242
}
4343

4444
func handleEntriesWithoutIssue(timeEntries []tmetric.TimeEntry, tmetricUser tmetric.User, config *config.Config) error {
45-
// get all entries that belong to the client and do not have an external link
46-
var entriesWithoutIssue []tmetric.TimeEntry
47-
for _, entry := range timeEntries {
48-
if entry.Project.Client.Id == config.ClientIdInTmetric && entry.Task.ExternalLink.IssueId == "" {
49-
entriesWithoutIssue = append(entriesWithoutIssue, entry)
50-
}
51-
}
52-
53-
if len(entriesWithoutIssue) > 0 {
45+
entriesWithoutLinkToOpenProject := tmetric.GetEntriesWithoutLinkToOpenProject(config, timeEntries)
46+
if len(entriesWithoutLinkToOpenProject) > 0 {
5447
fmt.Println("Some time-entries do not have any workpackages assigned")
5548
}
5649

5750
spinner := newSpinner()
5851
defer spinner.Stop()
5952

60-
for _, entry := range entriesWithoutIssue {
53+
for _, entry := range entriesWithoutLinkToOpenProject {
6154
prompt := promptui.Prompt{
6255
Label: fmt.Sprintf(
6356
"%v => %v %v-%v. Provide a WP number to be assigned to this time-entry (Enter to skip)",

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Config struct {
3232
TmetricAPIV3BaseUrl string
3333
TmetricDummyProjectId int
3434
TmetricTagTransferredToOpenProject string
35+
TmetricExternalTaskLink string
3536
}
3637

3738
func NewConfig() *Config {
@@ -69,5 +70,8 @@ func NewConfig() *Config {
6970
TmetricAPIV3BaseUrl: "https://app.tmetric.com/api/v3/",
7071
TmetricDummyProjectId: tmetricDummyProjectId,
7172
TmetricTagTransferredToOpenProject: "transferred-to-openproject",
73+
// this value has always to be "https://community.openproject.org"
74+
// otherwise tmetric does not recognize the integration and does not allow to create the external task
75+
TmetricExternalTaskLink: "https://community.openproject.org/",
7276
}
7377
}

tmetric/timeentry.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,18 @@ func (timeEntry *TimeEntry) GetPossibleWorkTypes(config config.Config, user User
174174
// GetIssueIdAsInt returns the issue id as an integer
175175
// the issue Id in tmetric is a string e.g. #1234, but for OpenProject we need the integer to construct the URLs
176176
func (timeEntry *TimeEntry) GetIssueIdAsInt() (int, error) {
177-
issueIdStr := regexp.MustCompile(`#(\d+)`).
178-
FindStringSubmatch(timeEntry.Task.ExternalLink.IssueId)[1]
179-
return strconv.Atoi(issueIdStr)
177+
issueRegex, err := regexp.Compile(`#(\d+)`)
178+
if err != nil {
179+
return 0, err
180+
}
181+
issueIdStr := issueRegex.FindStringSubmatch(timeEntry.Task.ExternalLink.IssueId)
182+
if len(issueIdStr) != 2 {
183+
return 0, fmt.Errorf(
184+
"could not find valid OpenProject workpackage id in tmetric task '%v'",
185+
timeEntry.Task.ExternalLink.IssueId,
186+
)
187+
}
188+
return strconv.Atoi(issueIdStr[1])
180189
}
181190

182191
/*
@@ -187,9 +196,7 @@ func CreateDummyTimeEntry(
187196
workPackage openproject.WorkPackage, tmetricUser User, config *config.Config,
188197
) (*TimeEntry, error) {
189198
dummyTimeEntry := NewDummyTimeEntry(workPackage, config.OpenProjectUrl, config.TmetricDummyProjectId)
190-
// the serviceUrl for the dummy task has always to be "https://community.openproject.org"
191-
// otherwise tmetric does not recognize the integration and does not allow to create the external task
192-
dummyTimeEntry.ServiceUrl = "https://community.openproject.org"
199+
dummyTimeEntry.ServiceUrl = config.TmetricExternalTaskLink
193200
dummyTimerString, _ := json.Marshal(dummyTimeEntry)
194201
httpClient := resty.New()
195202
resp, err := httpClient.R().

tmetric/tmetric.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/JankariTech/OpenProjectTmetricIntegration/config"
77
"github.com/go-resty/resty/v2"
88
"sort"
9+
"strings"
910
)
1011

1112
func GetAllTimeEntries(config *config.Config, tmetricUser User, startDate string, endDate string) ([]TimeEntry, error) {
@@ -83,10 +84,14 @@ func GetEntriesWithoutWorkType(timeEntries []TimeEntry) []TimeEntry {
8384
return entriesWithoutWorkType
8485
}
8586

86-
func GetEntriesWithoutLinkToOpenProject(timeEntries []TimeEntry) []TimeEntry {
87+
func GetEntriesWithoutLinkToOpenProject(config *config.Config, timeEntries []TimeEntry) []TimeEntry {
8788
var entriesWithoutLink []TimeEntry
8889
for _, entry := range timeEntries {
89-
if entry.Task.Id == 0 {
90+
_, err := entry.GetIssueIdAsInt()
91+
if entry.Task.Id == 0 ||
92+
err != nil ||
93+
entry.Task.ExternalLink.IssueId == "" ||
94+
(!strings.HasPrefix(entry.Task.ExternalLink.Link, config.TmetricExternalTaskLink+"work_packages")) {
9095
entriesWithoutLink = append(entriesWithoutLink, entry)
9196
}
9297
}

tmetric/tmetric_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,97 @@ import (
88
"testing"
99
)
1010

11+
func Test_GetEntriesWithoutLinkToOpenProject(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
task Task
15+
}{
16+
{
17+
name: "empty task",
18+
task: Task{},
19+
},
20+
{
21+
name: "no external link",
22+
task: Task{
23+
Id: 123,
24+
Name: "some WP",
25+
},
26+
},
27+
{
28+
name: "link is not to openproject",
29+
task: Task{
30+
Id: 345,
31+
Name: "some WP",
32+
ExternalLink: ExternalLink{
33+
Link: "https://some_host/work_packages/123",
34+
IssueId: "#123",
35+
},
36+
},
37+
},
38+
{
39+
name: "IssueId is empty",
40+
task: Task{
41+
Id: 345,
42+
Name: "some WP",
43+
ExternalLink: ExternalLink{
44+
Link: "https://community.openproject.org/work_packages/123",
45+
IssueId: "",
46+
},
47+
},
48+
},
49+
{
50+
name: "IssueId has wrong format",
51+
task: Task{
52+
Id: 345,
53+
Name: "some WP",
54+
ExternalLink: ExternalLink{
55+
Link: "https://community.openproject.org/work_packages/123",
56+
IssueId: "!123",
57+
},
58+
},
59+
},
60+
}
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
// adds a correct entry at the beginning and the end and makes sure the invalid
64+
// entry is still in the result
65+
config := &config.Config{
66+
ClientIdInTmetric: 123,
67+
TmetricExternalTaskLink: "https://community.openproject.org/",
68+
}
69+
validTimeEntry := TimeEntry{
70+
Project: Project{
71+
Client: Client{Id: 123},
72+
Name: "Project1",
73+
},
74+
Note: "correct entry",
75+
Task: Task{
76+
Id: 345,
77+
Name: "some WP",
78+
ExternalLink: ExternalLink{
79+
Link: "https://community.openproject.org/work_packages/123",
80+
IssueId: "#123",
81+
},
82+
},
83+
}
84+
invalidTimeEntry := TimeEntry{
85+
Project: Project{
86+
Client: Client{Id: 123},
87+
Name: "Project1",
88+
},
89+
Note: "invalid entry",
90+
Task: tt.task,
91+
}
92+
timeEntriesToCheck := append([]TimeEntry{invalidTimeEntry}, validTimeEntry)
93+
timeEntriesToCheck = append([]TimeEntry{validTimeEntry}, timeEntriesToCheck...)
94+
result := GetEntriesWithoutLinkToOpenProject(config, timeEntriesToCheck)
95+
if !reflect.DeepEqual(result, []TimeEntry{invalidTimeEntry}) {
96+
t.Errorf("got %v, want %v", result, []TimeEntry{invalidTimeEntry})
97+
}
98+
})
99+
}
100+
}
101+
11102
func Test_GetEntriesWithoutWorkType(t *testing.T) {
12103
type args struct {
13104
timeEntries []TimeEntry

0 commit comments

Comments
 (0)