Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions pkg/data/turns.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,22 +534,37 @@ func resetTurns(ctx workflow.Context, url string, t *PRTurns) (*PRTurns, error)

// Otherwise, recreate the entire struct and file.
reviewers := map[string]bool{}
jsonList, ok := pr["reviewers"].([]any)
if !ok {
jsonList = []any{}
}
for _, r := range jsonList {
if email, _ := userEmailAndType(ctx, r); email != "" {
jsonList := listOf(pr, "reviewers")
for _, reviewer := range jsonList {
if email, _ := userEmailAndType(ctx, reviewer); email != "" {
reviewers[email] = true
}
}

approvers := map[string]time.Time{}
activity, approvers := map[string]time.Time{}, map[string]time.Time{}
jsonList = listOf(pr, "participants")
for _, participant := range jsonList {
if email, approved, timestamp := userActivity(ctx, participant); email != "" {
activity[email] = timestamp
if approved {
approvers[email] = timestamp
}
}
}

t = &PRTurns{Author: author, Reviewers: reviewers, Activity: map[string]time.Time{}, Approvers: approvers}
t = &PRTurns{Author: author, Reviewers: reviewers, Activity: activity, Approvers: approvers}
return t, writeTurnsFile(ctx, url, t)
}

// listOf converts a given field in a PR's snapshot into a slice.
func listOf(pr map[string]any, key string) []any {
jsonList, ok := pr[key].([]any)
if !ok {
return []any{}
}
return jsonList
}

// userEmailAndType extracts the Bitbucket account ID from user details map, and converts
// it into the user's email address and account type, based on RevChat's own user database.
func userEmailAndType(ctx workflow.Context, detailsMap any) (email, accountType string) {
Expand All @@ -570,6 +585,32 @@ func userEmailAndType(ctx workflow.Context, detailsMap any) (email, accountType
return BitbucketIDToEmail(ctx, accountID, accountType), accountType
}

// userActivity extracts a user's email, approval status, and activity
// time from a JSON block of participant details in a Bitbucket PR snapshot.
func userActivity(ctx workflow.Context, detailsMap any) (string, bool, time.Time) {
participant, ok := detailsMap.(map[string]any)
if !ok {
return "", false, time.Time{}
}

email, _ := userEmailAndType(ctx, participant)
approved, ok := participant["approved"].(bool)
if !ok {
approved = false
}

t, ok := participant["participated_on"].(string)
if !ok {
return email, approved, time.Time{}
}
parsedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return email, approved, time.Time{}
}

return email, approved, parsedTime
}

// BitbucketIDToEmail converts a Bitbucket account ID into an email address. This function returns an empty
// string if the account ID is not found. It uses persistent data storage, or API calls as a fallback.
// This function is also wrapped in the "users" package, and reused by other packages from there.
Expand Down
102 changes: 102 additions & 0 deletions pkg/data/turns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package data
import (
"reflect"
"testing"
"time"
)

func TestTurns(t *testing.T) {
Expand Down Expand Up @@ -550,3 +551,104 @@ func TestNormalizeEmailAddresses(t *testing.T) {
t.Fatalf("normalizeEmailAddresses() Reviewers = %v, want %v", turn.Reviewers, wantReviewers)
}
}

func TestListOf(t *testing.T) {
tests := []struct {
name string
pr map[string]any
key string
want []any
}{
{
name: "missing_key",
pr: map[string]any{},
key: "key",
want: []any{},
},
{
name: "good_key",
pr: map[string]any{
"key": []any{"a", "b", "c"},
},
key: "key",
want: []any{"a", "b", "c"},
},
{
name: "wrong_type",
pr: map[string]any{
"key": "not a list",
},
key: "key",
want: []any{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := listOf(tt.pr, tt.key); !reflect.DeepEqual(got, tt.want) {
t.Errorf("listOf() = %v, want %v", got, tt.want)
}
})
}
}

func TestUserActivity(t *testing.T) {
tests := []struct {
name string
detailsMap any
wantApproved bool
wantZero bool
}{
{
name: "invalid_participant",
detailsMap: "not a map",
wantZero: true,
},
{
name: "invalid_user",
detailsMap: map[string]any{
"user": "not a map",
},
wantZero: true,
},
{
name: "missing_approved",
detailsMap: map[string]any{
"user": map[string]any{},
},
wantZero: true,
},
{
name: "invalid_approved",
detailsMap: map[string]any{
"user": map[string]any{},
"approved": "not a bool",
},
wantZero: true,
},
{
name: "approved",
detailsMap: map[string]any{
"user": map[string]any{},
"approved": true,
"participated_on": time.Now().UTC().Format(time.RFC3339),
},
wantApproved: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
email, approved, ts := userActivity(nil, tt.detailsMap)
if email != "" {
t.Errorf("userActivity() email = %q, want %q", email, "")
}
if approved != tt.wantApproved {
t.Errorf("userActivity() approved = %v, want %v", approved, tt.wantApproved)
}
if ts.IsZero() != tt.wantZero {
t.Errorf("userActivity() timestamp = %v, want zero: %v", ts, tt.wantZero)
}
})
}
}