Files
core/internal/idgen/generator_test.go

397 lines
8.2 KiB
Go

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
}