397 lines
8.2 KiB
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
|
|
}
|