package idgen import ( "context" "database/sql" "sync" "testing" _ "github.com/go-sql-driver/mysql" ) // Helper to create test database connection func setupTestDB(t *testing.T) *sql.DB { t.Helper() // Use test database connection db, err := sql.Open("mysql", "root:root@tcp(localhost:3306)/reanimator_test?parseTime=true") if err != nil { t.Fatalf("Failed to connect to test database: %v", err) } if err := db.Ping(); err != nil { t.Fatalf("Failed to ping test database: %v", err) } return db } // Helper to initialize id_sequences table for testing func initSequences(t *testing.T, db *sql.DB) { t.Helper() ctx := context.Background() // Drop and recreate table _, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS id_sequences`) if err != nil { t.Fatalf("Failed to drop id_sequences table: %v", err) } _, err = db.ExecContext(ctx, ` CREATE TABLE id_sequences ( entity_type VARCHAR(32) PRIMARY KEY, next_value BIGINT NOT NULL DEFAULT 1, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) `) if err != nil { t.Fatalf("Failed to create id_sequences table: %v", err) } // Insert initial sequences entities := []EntityType{ Customer, Project, Location, Lot, Asset, Component, Installation, LogBundle, Observation, TimelineEvent, Ticket, TicketLink, FailureEvent, } for _, entity := range entities { _, err := db.ExecContext(ctx, `INSERT INTO id_sequences (entity_type, next_value) VALUES (?, 1)`, string(entity), ) if err != nil { t.Fatalf("Failed to insert sequence for %s: %v", entity, err) } } } func TestGenerator_Generate(t *testing.T) { db := setupTestDB(t) defer db.Close() initSequences(t, db) gen := NewGenerator(db) ctx := context.Background() tests := []struct { name string entityType EntityType wantPrefix string wantFormat string }{ { name: "customer ID", entityType: Customer, wantPrefix: "CR", wantFormat: "CR-0000001", }, { name: "project ID", entityType: Project, wantPrefix: "PJ", wantFormat: "PJ-0000001", }, { name: "location ID", entityType: Location, wantPrefix: "LN", wantFormat: "LN-0000001", }, { name: "asset ID (future machine)", entityType: Asset, wantPrefix: "ME", wantFormat: "ME-0000001", }, { name: "component ID (future part)", entityType: Component, wantPrefix: "PT", wantFormat: "PT-0000001", }, { name: "ticket ID", entityType: Ticket, wantPrefix: "TT", wantFormat: "TT-0000001", }, { name: "failure event ID", entityType: FailureEvent, wantPrefix: "FE", wantFormat: "FE-0000001", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { id, err := gen.Generate(ctx, tt.entityType) if err != nil { t.Fatalf("Generate() error = %v", err) } if id != tt.wantFormat { t.Errorf("Generate() = %v, want %v", id, tt.wantFormat) } }) } } func TestGenerator_GenerateSequential(t *testing.T) { db := setupTestDB(t) defer db.Close() initSequences(t, db) gen := NewGenerator(db) ctx := context.Background() // Generate multiple IDs for the same entity type expected := []string{ "CR-0000001", "CR-0000002", "CR-0000003", "CR-0000004", "CR-0000005", } for i, want := range expected { id, err := gen.Generate(ctx, Customer) if err != nil { t.Fatalf("Generate() iteration %d error = %v", i, err) } if id != want { t.Errorf("Generate() iteration %d = %v, want %v", i, id, want) } } } func TestGenerator_GenerateConcurrent(t *testing.T) { db := setupTestDB(t) defer db.Close() initSequences(t, db) gen := NewGenerator(db) ctx := context.Background() // Number of concurrent goroutines numGoroutines := 50 // Channel to collect generated IDs ids := make(chan string, numGoroutines) // WaitGroup to coordinate goroutines var wg sync.WaitGroup wg.Add(numGoroutines) // Launch concurrent ID generators for i := 0; i < numGoroutines; i++ { go func() { defer wg.Done() id, err := gen.Generate(ctx, Customer) if err != nil { t.Errorf("Generate() error = %v", err) return } ids <- id }() } // Wait for all goroutines to complete wg.Wait() close(ids) // Collect all IDs seen := make(map[string]bool) count := 0 for id := range ids { count++ if seen[id] { t.Errorf("Duplicate ID generated: %s", id) } seen[id] = true // Verify format var num int64 var prefix string _, err := parseIDParts(id, &prefix, &num) if err != nil { t.Errorf("Invalid ID format: %s, error: %v", id, err) } if prefix != "CR" { t.Errorf("Wrong prefix: %s, want CR", prefix) } if num < 1 || num > int64(numGoroutines) { t.Errorf("ID number out of range: %d", num) } } if count != numGoroutines { t.Errorf("Expected %d IDs, got %d", numGoroutines, count) } } func TestGenerator_GenerateUnknownEntityType(t *testing.T) { db := setupTestDB(t) defer db.Close() initSequences(t, db) gen := NewGenerator(db) ctx := context.Background() _, err := gen.Generate(ctx, EntityType("unknown")) if err == nil { t.Error("Generate() expected error for unknown entity type, got nil") } } func TestFormatID(t *testing.T) { tests := []struct { prefix string number int64 want string }{ {"CR", 1, "CR-0000001"}, {"CR", 42, "CR-0000042"}, {"CR", 999, "CR-0000999"}, {"CR", 1000, "CR-0001000"}, {"CR", 9999999, "CR-9999999"}, {"PJ", 1, "PJ-0000001"}, {"ME", 123456, "ME-0123456"}, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { got := FormatID(tt.prefix, tt.number) if got != tt.want { t.Errorf("FormatID(%s, %d) = %v, want %v", tt.prefix, tt.number, got, tt.want) } }) } } func TestParseID(t *testing.T) { tests := []struct { id string want int64 wantErr bool }{ {"CR-0000001", 1, false}, {"CR-0000042", 42, false}, {"CR-0000999", 999, false}, {"CR-0001000", 1000, false}, {"CR-9999999", 9999999, false}, {"PJ-0000001", 1, false}, {"ME-0123456", 123456, false}, {"invalid", 0, true}, {"CR-", 0, true}, {"CR-abc", 0, true}, {"-0000001", 0, true}, } for _, tt := range tests { t.Run(tt.id, func(t *testing.T) { got, err := ParseID(tt.id) if (err != nil) != tt.wantErr { t.Errorf("ParseID(%s) error = %v, wantErr %v", tt.id, err, tt.wantErr) return } if !tt.wantErr && got != tt.want { t.Errorf("ParseID(%s) = %v, want %v", tt.id, got, tt.want) } }) } } func TestGetPrefix(t *testing.T) { tests := []struct { entityType EntityType want string wantErr bool }{ {Customer, "CR", false}, {Project, "PJ", false}, {Location, "LN", false}, {Asset, "ME", false}, {Component, "PT", false}, {Ticket, "TT", false}, {FailureEvent, "FE", false}, {EntityType("unknown"), "", true}, } for _, tt := range tests { t.Run(string(tt.entityType), func(t *testing.T) { got, err := GetPrefix(tt.entityType) if (err != nil) != tt.wantErr { t.Errorf("GetPrefix(%s) error = %v, wantErr %v", tt.entityType, err, tt.wantErr) return } if !tt.wantErr && got != tt.want { t.Errorf("GetPrefix(%s) = %v, want %v", tt.entityType, got, tt.want) } }) } } // Helper function to parse ID parts (for testing) func parseIDParts(id string, prefix *string, number *int64) (int, error) { return scanIDParts(id, prefix, number) } func scanIDParts(id string, prefix *string, number *int64) (int, error) { var p [3]byte // 2 chars + null terminator n, err := parseIDInternal(id, p[:], number) if err != nil { return n, err } *prefix = string(p[:2]) return n, nil } func parseIDInternal(id string, prefix []byte, number *int64) (int, error) { if len(id) < 10 { // "XX-0000000" minimum return 0, ParseError{ID: id} } // Extract prefix (first 2 chars) copy(prefix, id[:2]) // Check separator if id[2] != '-' { return 0, ParseError{ID: id} } // Parse number part var num int64 for i := 3; i < len(id); i++ { if id[i] < '0' || id[i] > '9' { return 0, ParseError{ID: id} } num = num*10 + int64(id[i]-'0') } *number = num return 2, nil } type ParseError struct { ID string } func (e ParseError) Error() string { return "invalid ID format: " + e.ID }