Add actual timestamp comparison to detect conflicts during Jira sync: - Add fetchJiraIssueTimestamp() to fetch a single issue's updated timestamp from Jira REST API - Add extractJiraKey() to parse Jira issue key from external_ref URLs - Add parseJiraTimestamp() to parse Jira's ISO 8601 timestamp format - Update detectJiraConflicts() to fetch and compare Jira timestamps instead of marking all locally-updated issues as conflicts - Update resolveConflictsByTimestamp() to show actual timestamp comparison results - Update reimportConflicts() to display both local and Jira timestamps Now only issues that have been modified on BOTH sides since the last sync are reported as conflicts, reducing false positives. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
295 lines
7.0 KiB
Go
295 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestIsJiraExternalRef(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
externalRef string
|
|
jiraURL string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "valid Jira Cloud URL",
|
|
externalRef: "https://company.atlassian.net/browse/PROJ-123",
|
|
jiraURL: "https://company.atlassian.net",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid Jira Cloud URL with trailing slash in config",
|
|
externalRef: "https://company.atlassian.net/browse/PROJ-123",
|
|
jiraURL: "https://company.atlassian.net/",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "valid Jira Server URL",
|
|
externalRef: "https://jira.company.com/browse/PROJ-456",
|
|
jiraURL: "https://jira.company.com",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "mismatched Jira host",
|
|
externalRef: "https://other.atlassian.net/browse/PROJ-123",
|
|
jiraURL: "https://company.atlassian.net",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "GitHub issue URL",
|
|
externalRef: "https://github.com/org/repo/issues/123",
|
|
jiraURL: "https://company.atlassian.net",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "empty external_ref",
|
|
externalRef: "",
|
|
jiraURL: "https://company.atlassian.net",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no jiraURL configured - valid pattern",
|
|
externalRef: "https://any.atlassian.net/browse/PROJ-123",
|
|
jiraURL: "",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no jiraURL configured - invalid pattern",
|
|
externalRef: "https://github.com/org/repo/issues/123",
|
|
jiraURL: "",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "browse in path but not Jira format",
|
|
externalRef: "https://example.com/browse/docs/page",
|
|
jiraURL: "",
|
|
want: true, // Contains /browse/, so matches pattern
|
|
},
|
|
{
|
|
name: "browse in path with jiraURL check",
|
|
externalRef: "https://example.com/browse/docs/page",
|
|
jiraURL: "https://company.atlassian.net",
|
|
want: false, // Host doesn't match
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isJiraExternalRef(tt.externalRef, tt.jiraURL)
|
|
if got != tt.want {
|
|
t.Errorf("isJiraExternalRef(%q, %q) = %v, want %v",
|
|
tt.externalRef, tt.jiraURL, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJiraSyncStats(t *testing.T) {
|
|
// Test that stats struct initializes correctly
|
|
stats := JiraSyncStats{}
|
|
|
|
if stats.Pulled != 0 {
|
|
t.Errorf("expected Pulled to be 0, got %d", stats.Pulled)
|
|
}
|
|
if stats.Pushed != 0 {
|
|
t.Errorf("expected Pushed to be 0, got %d", stats.Pushed)
|
|
}
|
|
if stats.Created != 0 {
|
|
t.Errorf("expected Created to be 0, got %d", stats.Created)
|
|
}
|
|
if stats.Updated != 0 {
|
|
t.Errorf("expected Updated to be 0, got %d", stats.Updated)
|
|
}
|
|
if stats.Skipped != 0 {
|
|
t.Errorf("expected Skipped to be 0, got %d", stats.Skipped)
|
|
}
|
|
if stats.Errors != 0 {
|
|
t.Errorf("expected Errors to be 0, got %d", stats.Errors)
|
|
}
|
|
if stats.Conflicts != 0 {
|
|
t.Errorf("expected Conflicts to be 0, got %d", stats.Conflicts)
|
|
}
|
|
}
|
|
|
|
func TestJiraSyncResult(t *testing.T) {
|
|
// Test result struct initialization
|
|
result := JiraSyncResult{
|
|
Success: true,
|
|
Stats: JiraSyncStats{
|
|
Created: 5,
|
|
Updated: 3,
|
|
},
|
|
LastSync: "2025-01-15T10:30:00Z",
|
|
}
|
|
|
|
if !result.Success {
|
|
t.Error("expected Success to be true")
|
|
}
|
|
if result.Stats.Created != 5 {
|
|
t.Errorf("expected Created to be 5, got %d", result.Stats.Created)
|
|
}
|
|
if result.Stats.Updated != 3 {
|
|
t.Errorf("expected Updated to be 3, got %d", result.Stats.Updated)
|
|
}
|
|
if result.LastSync != "2025-01-15T10:30:00Z" {
|
|
t.Errorf("unexpected LastSync value: %s", result.LastSync)
|
|
}
|
|
if result.Error != "" {
|
|
t.Errorf("expected Error to be empty, got %s", result.Error)
|
|
}
|
|
if len(result.Warnings) != 0 {
|
|
t.Errorf("expected Warnings to be empty, got %v", result.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestPullStats(t *testing.T) {
|
|
stats := PullStats{
|
|
Created: 10,
|
|
Updated: 5,
|
|
Skipped: 2,
|
|
}
|
|
|
|
if stats.Created != 10 {
|
|
t.Errorf("expected Created to be 10, got %d", stats.Created)
|
|
}
|
|
if stats.Updated != 5 {
|
|
t.Errorf("expected Updated to be 5, got %d", stats.Updated)
|
|
}
|
|
if stats.Skipped != 2 {
|
|
t.Errorf("expected Skipped to be 2, got %d", stats.Skipped)
|
|
}
|
|
}
|
|
|
|
func TestPushStats(t *testing.T) {
|
|
stats := PushStats{
|
|
Created: 8,
|
|
Updated: 4,
|
|
Skipped: 1,
|
|
Errors: 2,
|
|
}
|
|
|
|
if stats.Created != 8 {
|
|
t.Errorf("expected Created to be 8, got %d", stats.Created)
|
|
}
|
|
if stats.Updated != 4 {
|
|
t.Errorf("expected Updated to be 4, got %d", stats.Updated)
|
|
}
|
|
if stats.Skipped != 1 {
|
|
t.Errorf("expected Skipped to be 1, got %d", stats.Skipped)
|
|
}
|
|
if stats.Errors != 2 {
|
|
t.Errorf("expected Errors to be 2, got %d", stats.Errors)
|
|
}
|
|
}
|
|
|
|
func TestExtractJiraKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
externalRef string
|
|
want string
|
|
}{
|
|
{
|
|
name: "standard Jira Cloud URL",
|
|
externalRef: "https://company.atlassian.net/browse/PROJ-123",
|
|
want: "PROJ-123",
|
|
},
|
|
{
|
|
name: "Jira Server URL",
|
|
externalRef: "https://jira.company.com/browse/ISSUE-456",
|
|
want: "ISSUE-456",
|
|
},
|
|
{
|
|
name: "URL with trailing path",
|
|
externalRef: "https://company.atlassian.net/browse/ABC-789/some/path",
|
|
want: "ABC-789/some/path",
|
|
},
|
|
{
|
|
name: "no browse pattern",
|
|
externalRef: "https://github.com/org/repo/issues/123",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
externalRef: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "only browse",
|
|
externalRef: "https://example.com/browse/",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractJiraKey(tt.externalRef)
|
|
if got != tt.want {
|
|
t.Errorf("extractJiraKey(%q) = %q, want %q", tt.externalRef, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseJiraTimestamp(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
timestamp string
|
|
wantErr bool
|
|
wantYear int
|
|
}{
|
|
{
|
|
name: "standard Jira Cloud format with milliseconds",
|
|
timestamp: "2024-01-15T10:30:00.000+0000",
|
|
wantErr: false,
|
|
wantYear: 2024,
|
|
},
|
|
{
|
|
name: "Jira format with Z suffix",
|
|
timestamp: "2024-01-15T10:30:00.000Z",
|
|
wantErr: false,
|
|
wantYear: 2024,
|
|
},
|
|
{
|
|
name: "without milliseconds",
|
|
timestamp: "2024-01-15T10:30:00+0000",
|
|
wantErr: false,
|
|
wantYear: 2024,
|
|
},
|
|
{
|
|
name: "RFC3339 format",
|
|
timestamp: "2024-01-15T10:30:00Z",
|
|
wantErr: false,
|
|
wantYear: 2024,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
timestamp: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid format",
|
|
timestamp: "not-a-timestamp",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "with negative timezone offset",
|
|
timestamp: "2024-06-15T10:30:00.000-0500",
|
|
wantErr: false,
|
|
wantYear: 2024,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseJiraTimestamp(tt.timestamp)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseJiraTimestamp(%q) error = %v, wantErr %v", tt.timestamp, err, tt.wantErr)
|
|
return
|
|
}
|
|
if !tt.wantErr && got.Year() != tt.wantYear {
|
|
t.Errorf("parseJiraTimestamp(%q) year = %d, want %d", tt.timestamp, got.Year(), tt.wantYear)
|
|
}
|
|
})
|
|
}
|
|
}
|