summaryrefslogtreecommitdiff
path: root/internal/commands/import_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/commands/import_test.go')
-rw-r--r--internal/commands/import_test.go543
1 files changed, 543 insertions, 0 deletions
diff --git a/internal/commands/import_test.go b/internal/commands/import_test.go
new file mode 100644
index 0000000..ed59f92
--- /dev/null
+++ b/internal/commands/import_test.go
@@ -0,0 +1,543 @@
+package commands
+
+import (
+ "context"
+ "database/sql"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "punchcard/internal/queries"
+)
+
+func TestImportCommand(t *testing.T) {
+ tests := []struct {
+ name string
+ setupData func(*queries.Queries) error
+ csvFile string
+ args []string
+ expectedOutputs []string
+ expectError bool
+ errorContains string
+ validateDatabase func(*testing.T, *queries.Queries)
+ }{
+ {
+ name: "successful import with full CSV format",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "America/New_York"},
+ expectedOutputs: []string{"Successfully imported 3 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ // Check clients were created
+ clients, err := q.ListAllClients(context.Background())
+ if err != nil {
+ t.Errorf("Failed to list clients: %v", err)
+ return
+ }
+ expectedClients := map[string]bool{
+ "Acme Corp": false, "Creative Co": false,
+ }
+ for _, client := range clients {
+ if _, exists := expectedClients[client.Name]; exists {
+ expectedClients[client.Name] = true
+ }
+ }
+ for clientName, found := range expectedClients {
+ if !found {
+ t.Errorf("Expected client %q not found", clientName)
+ }
+ }
+
+ // Check projects were created
+ projects, err := q.ListAllProjects(context.Background())
+ if err != nil {
+ t.Errorf("Failed to list projects: %v", err)
+ return
+ }
+ expectedProjects := map[string]bool{
+ "Project Alpha": false, "Project Beta": false, "Website Redesign": false,
+ }
+ for _, project := range projects {
+ if _, exists := expectedProjects[project.Name]; exists {
+ expectedProjects[project.Name] = true
+ }
+ }
+ for projectName, found := range expectedProjects {
+ if !found {
+ t.Errorf("Expected project %q not found", projectName)
+ }
+ }
+ },
+ },
+ {
+ name: "successful import without billable columns",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_no_billable.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "UTC"},
+ expectedOutputs: []string{"Successfully imported 2 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ // Check TechStart Inc client was created
+ client, err := q.GetClientByName(context.Background(), "TechStart Inc")
+ if err != nil {
+ t.Errorf("Expected client 'TechStart Inc' not found: %v", err)
+ return
+ }
+
+ // Check projects were created under correct client
+ project1, err := q.GetProjectByNameAndClient(context.Background(), queries.GetProjectByNameAndClientParams{
+ Name: "Project Gamma", ClientID: client.ID,
+ })
+ if err != nil {
+ t.Errorf("Expected project 'Project Gamma' not found: %v", err)
+ }
+
+ project2, err := q.GetProjectByNameAndClient(context.Background(), queries.GetProjectByNameAndClientParams{
+ Name: "Mobile App", ClientID: client.ID,
+ })
+ if err != nil {
+ t.Errorf("Expected project 'Mobile App' not found: %v", err)
+ }
+
+ // Verify the projects belong to the correct client
+ if project1.ClientID != client.ID {
+ t.Errorf("Project Gamma should belong to client %d, got %d", client.ID, project1.ClientID)
+ }
+ if project2.ClientID != client.ID {
+ t.Errorf("Mobile App should belong to client %d, got %d", client.ID, project2.ClientID)
+ }
+ },
+ },
+ {
+ name: "timezone conversion test",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "UTC"},
+ expectedOutputs: []string{"Successfully imported 3 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ // We can't easily test the exact timezone conversion without more complex setup,
+ // but we can verify that entries were created and have valid timestamps
+ // This is more of an integration test that the timezone parsing doesn't crash
+ },
+ },
+ {
+ name: "duplicate client/project handling",
+ setupData: func(q *queries.Queries) error {
+ // Pre-create a client that exists in the CSV
+ _, err := q.CreateClient(context.Background(), queries.CreateClientParams{
+ Name: "Acme Corp",
+ Email: sql.NullString{String: "existing@acme.com", Valid: true},
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+ },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Local"},
+ expectedOutputs: []string{"Successfully imported 3 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ // Check that we don't have duplicate clients
+ clients, err := q.ListAllClients(context.Background())
+ if err != nil {
+ t.Errorf("Failed to list clients: %v", err)
+ return
+ }
+
+ acmeCount := 0
+ for _, client := range clients {
+ if client.Name == "Acme Corp" {
+ acmeCount++
+ }
+ }
+
+ if acmeCount != 1 {
+ t.Errorf("Expected exactly 1 'Acme Corp' client, found %d", acmeCount)
+ }
+ },
+ },
+ {
+ name: "missing client/project data handling",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_missing_data.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Local"},
+ expectedOutputs: []string{"Successfully imported 1 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ // Only the first entry should be imported (has both client and project)
+ client, err := q.GetClientByName(context.Background(), "Valid Client")
+ if err != nil {
+ t.Errorf("Expected client 'Valid Client' not found: %v", err)
+ }
+
+ project, err := q.GetProjectByNameAndClient(context.Background(), queries.GetProjectByNameAndClientParams{
+ Name: "Valid Project", ClientID: client.ID,
+ })
+ if err != nil {
+ t.Errorf("Expected project 'Valid Project' not found: %v", err)
+ }
+
+ if project.ClientID != client.ID {
+ t.Errorf("Project should belong to client %d, got %d", client.ID, project.ClientID)
+ }
+ },
+ },
+ {
+ name: "invalid CSV format",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_invalid.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Local"},
+ expectError: true,
+ errorContains: "wrong number of fields",
+ },
+ {
+ name: "nonexistent file",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "nonexistent.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Local"},
+ expectError: true,
+ errorContains: "failed to open file",
+ },
+ {
+ name: "invalid timezone",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Invalid/Timezone"},
+ expectError: true,
+ errorContains: "invalid timezone",
+ },
+ {
+ name: "missing source flag",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--timezone", "Local"},
+ expectError: true,
+ errorContains: "required flag(s) \"source\" not set",
+ },
+ {
+ name: "empty source flag",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "", "--timezone", "Local"},
+ expectError: true,
+ errorContains: "required flag \"source\" not set",
+ },
+ {
+ name: "invalid source",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_full.csv",
+ args: []string{"import", "", "--source", "invalid", "--timezone", "Local"},
+ expectError: true,
+ errorContains: "unsupported source: invalid",
+ },
+ {
+ name: "reordered columns with extra fields",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_reordered.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "UTC"},
+ expectedOutputs: []string{"Successfully imported 2 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ client, err := q.GetClientByName(context.Background(), "MegaCorp Inc")
+ if err != nil {
+ t.Errorf("Expected client 'MegaCorp Inc' not found: %v", err)
+ return
+ }
+ project, err := q.GetProjectByNameAndClient(context.Background(), queries.GetProjectByNameAndClientParams{
+ Name: "Website Overhaul",
+ ClientID: client.ID,
+ })
+ if err != nil {
+ t.Errorf("Expected project 'Website Overhaul' not found: %v", err)
+ }
+ if project.ClientID != client.ID {
+ t.Errorf("Website Overhaul should belong to client %d, got %d", client.ID, project.ClientID)
+ }
+ },
+ },
+ {
+ name: "extra columns beyond required",
+ setupData: func(q *queries.Queries) error { return nil },
+ csvFile: "clockify_extra_columns.csv",
+ args: []string{"import", "", "--source", "clockify", "--timezone", "Local"},
+ expectedOutputs: []string{"Successfully imported 2 time entries"},
+ expectError: false,
+ validateDatabase: func(t *testing.T, q *queries.Queries) {
+ client, err := q.GetClientByName(context.Background(), "DataCorp")
+ if err != nil {
+ t.Errorf("Expected client 'DataCorp' not found: %v", err)
+ return
+ }
+ projects, err := q.ListAllProjects(context.Background())
+ if err != nil {
+ t.Errorf("Failed to list projects: %v", err)
+ return
+ }
+ expectedProjects := map[string]bool{
+ "Analytics Dashboard": false,
+ "API Development": false,
+ }
+ for _, project := range projects {
+ if _, exists := expectedProjects[project.Name]; exists {
+ expectedProjects[project.Name] = true
+ if project.ClientID != client.ID {
+ t.Errorf("Project %s should belong to client %d, got %d", project.Name, client.ID, project.ClientID)
+ }
+ }
+ }
+ for projectName, found := range expectedProjects {
+ if !found {
+ t.Errorf("Expected project %q not found", projectName)
+ }
+ }
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Setup fresh database for each test
+ q, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Setup test data if needed
+ if tt.setupData != nil {
+ if err := tt.setupData(q); err != nil {
+ t.Fatalf("Failed to setup test data: %v", err)
+ }
+ }
+
+ // Prepare file path
+ testDataPath := filepath.Join("testdata", tt.csvFile)
+ if tt.csvFile != "nonexistent.csv" {
+ // Verify test file exists
+ if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
+ t.Fatalf("Test data file %s does not exist", testDataPath)
+ }
+ }
+
+ // Update args with actual file path
+ args := make([]string, len(tt.args))
+ copy(args, tt.args)
+ if len(args) > 1 && args[1] == "" {
+ args[1] = testDataPath
+ }
+
+ // Execute command
+ output, err := executeCommandWithDB(t, q, args...)
+
+ // Check error expectation
+ if tt.expectError {
+ if err == nil {
+ t.Errorf("Expected error but got none. Output: %q, Args: %v", output, args)
+ } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
+ t.Errorf("Expected error to contain %q, got %q", tt.errorContains, err.Error())
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ // Note: Output validation is skipped because fmt.Printf writes to os.Stdout
+ // directly and is not captured by cobra's test framework.
+ // We rely on database validation instead for testing correctness.
+
+ // Run database validation if provided
+ if tt.validateDatabase != nil {
+ tt.validateDatabase(t, q)
+ }
+ })
+ }
+}
+
+func TestClockifyDateTimeParsing(t *testing.T) {
+ // Test the parseClockifyDateTime function with various formats
+ loc, err := time.LoadLocation("America/New_York")
+ if err != nil {
+ t.Fatalf("Failed to load timezone: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ date string
+ timeStr string
+ expected time.Time
+ }{
+ {
+ name: "12-hour format with AM",
+ date: "01/15/2024",
+ timeStr: "09:30:00 AM",
+ expected: time.Date(2024, 1, 15, 9, 30, 0, 0, loc),
+ },
+ {
+ name: "12-hour format with PM",
+ date: "01/15/2024",
+ timeStr: "02:45:30 PM",
+ expected: time.Date(2024, 1, 15, 14, 45, 30, 0, loc),
+ },
+ {
+ name: "24-hour format",
+ date: "01/15/2024",
+ timeStr: "15:20:45",
+ expected: time.Date(2024, 1, 15, 15, 20, 45, 0, loc),
+ },
+ {
+ name: "single digit month/day with 12-hour format",
+ date: "1/5/2024",
+ timeStr: "9:00:00 AM",
+ expected: time.Date(2024, 1, 5, 9, 0, 0, 0, loc),
+ },
+ {
+ name: "single digit month/day with 24-hour format",
+ date: "1/5/2024",
+ timeStr: "09:00:00",
+ expected: time.Date(2024, 1, 5, 9, 0, 0, 0, loc),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := parseClockifyDateTime(tt.date, tt.timeStr, loc)
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if !result.Equal(tt.expected) {
+ t.Errorf("Expected %v, got %v", tt.expected, result)
+ }
+ })
+ }
+
+ // Test error cases
+ errorTests := []struct {
+ name string
+ date string
+ timeStr string
+ }{
+ {"invalid date format", "2024-01-15", "09:00:00 AM"},
+ {"invalid time format", "01/15/2024", "25:00:00"},
+ {"empty date", "", "09:00:00 AM"},
+ {"empty time", "01/15/2024", ""},
+ }
+
+ for _, tt := range errorTests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := parseClockifyDateTime(tt.date, tt.timeStr, loc)
+ if err == nil {
+ t.Errorf("Expected error for invalid input, but got none")
+ }
+ })
+ }
+}
+
+func TestGetOrCreateClientAndProject(t *testing.T) {
+ // Test getOrCreateClient
+ t.Run("create new client", func(t *testing.T) {
+ q, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ client, err := getOrCreateClient(q, "New Client")
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if client.Name != "New Client" {
+ t.Errorf("Expected client name 'New Client', got %q", client.Name)
+ }
+ })
+
+ t.Run("get existing client", func(t *testing.T) {
+ q, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // First create a client
+ originalClient, err := q.CreateClient(context.Background(), queries.CreateClientParams{
+ Name: "Existing Client",
+ Email: sql.NullString{String: "existing@example.com", Valid: true},
+ })
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+
+ // Now try to get or create it again
+ client, err := getOrCreateClient(q, "Existing Client")
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if client.ID != originalClient.ID {
+ t.Errorf("Expected to get existing client with ID %d, got %d", originalClient.ID, client.ID)
+ }
+ })
+
+ // Test getOrCreateProject
+ t.Run("create new project", func(t *testing.T) {
+ q, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // First create a client
+ client, err := q.CreateClient(context.Background(), queries.CreateClientParams{
+ Name: "Project Client",
+ })
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+
+ project, err := getOrCreateProject(q, "New Project", client.ID)
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if project.Name != "New Project" {
+ t.Errorf("Expected project name 'New Project', got %q", project.Name)
+ }
+
+ if project.ClientID != client.ID {
+ t.Errorf("Expected project to belong to client %d, got %d", client.ID, project.ClientID)
+ }
+ })
+
+ t.Run("get existing project", func(t *testing.T) {
+ q, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // First create a client and project
+ client, err := q.CreateClient(context.Background(), queries.CreateClientParams{
+ Name: "Another Client",
+ })
+ if err != nil {
+ t.Fatalf("Failed to create client: %v", err)
+ }
+
+ originalProject, err := q.CreateProject(context.Background(), queries.CreateProjectParams{
+ Name: "Existing Project",
+ ClientID: client.ID,
+ })
+ if err != nil {
+ t.Fatalf("Failed to create project: %v", err)
+ }
+
+ // Now try to get or create it again
+ project, err := getOrCreateProject(q, "Existing Project", client.ID)
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+
+ if project.ID != originalProject.ID {
+ t.Errorf("Expected to get existing project with ID %d, got %d", originalProject.ID, project.ID)
+ }
+ })
+}