package partnum import ( "context" "fmt" "regexp" "testing" "time" "github.com/kindredsystems/silo/internal/schema" ) // mockSeqStore implements SequenceStore for testing. type mockSeqStore struct { counter int } func (m *mockSeqStore) NextValue(_ context.Context, _ string, _ string) (int, error) { m.counter++ return m.counter, nil } func testSchema() *schema.Schema { return &schema.Schema{ Name: "test", Version: 1, Separator: "-", Segments: []schema.Segment{ { Name: "category", Type: "enum", Required: true, Values: map[string]string{ "F01": "Fasteners", "R01": "Resistors", }, }, { Name: "serial", Type: "serial", Length: 4, Scope: "{category}", }, }, } } func TestGenerateBasic(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "test", Values: map[string]string{"category": "F01"}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } if pn != "F01-0001" { t.Errorf("got %q, want %q", pn, "F01-0001") } } func TestGenerateSequentialNumbers(t *testing.T) { s := testSchema() seq := &mockSeqStore{} gen := NewGenerator(map[string]*schema.Schema{"test": s}, seq) for i := 1; i <= 3; i++ { pn, err := gen.Generate(context.Background(), Input{ SchemaName: "test", Values: map[string]string{"category": "F01"}, }) if err != nil { t.Fatalf("Generate #%d returned error: %v", i, err) } want := fmt.Sprintf("F01-%04d", i) if pn != want { t.Errorf("Generate #%d: got %q, want %q", i, pn, want) } } } func TestGenerateWithFormat(t *testing.T) { s := &schema.Schema{ Name: "formatted", Version: 1, Format: "{prefix}/{category}-{serial}", Segments: []schema.Segment{ {Name: "prefix", Type: "constant", Value: "KS"}, {Name: "category", Type: "enum", Required: true, Values: map[string]string{"A": "Alpha"}}, {Name: "serial", Type: "serial", Length: 3}, }, } gen := NewGenerator(map[string]*schema.Schema{"formatted": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "formatted", Values: map[string]string{"category": "A"}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } if pn != "KS/A-001" { t.Errorf("got %q, want %q", pn, "KS/A-001") } } func TestGenerateUnknownSchema(t *testing.T) { gen := NewGenerator(map[string]*schema.Schema{}, &mockSeqStore{}) _, err := gen.Generate(context.Background(), Input{ SchemaName: "nonexistent", Values: map[string]string{}, }) if err == nil { t.Fatal("expected error for unknown schema, got nil") } } func TestGenerateMissingRequiredEnum(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) _, err := gen.Generate(context.Background(), Input{ SchemaName: "test", Values: map[string]string{}, // missing required "category" }) if err == nil { t.Fatal("expected error for missing required enum, got nil") } } func TestGenerateInvalidEnumValue(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) _, err := gen.Generate(context.Background(), Input{ SchemaName: "test", Values: map[string]string{"category": "INVALID"}, }) if err == nil { t.Fatal("expected error for invalid enum value, got nil") } } func TestGenerateConstantSegment(t *testing.T) { s := &schema.Schema{ Name: "const-test", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "prefix", Type: "constant", Value: "KS"}, {Name: "serial", Type: "serial", Length: 4}, }, } gen := NewGenerator(map[string]*schema.Schema{"const-test": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "const-test", Values: map[string]string{}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } if pn != "KS-0001" { t.Errorf("got %q, want %q", pn, "KS-0001") } } func TestGenerateDateSegmentDefault(t *testing.T) { s := &schema.Schema{ Name: "date-test", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "date", Type: "date"}, {Name: "serial", Type: "serial", Length: 3}, }, } gen := NewGenerator(map[string]*schema.Schema{"date-test": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "date-test", Values: map[string]string{}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } // Default format: YYYYMMDD-NNN want := time.Now().UTC().Format("20060102") + "-001" if pn != want { t.Errorf("got %q, want %q", pn, want) } } func TestGenerateDateSegmentCustomFormat(t *testing.T) { s := &schema.Schema{ Name: "date-custom", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "date", Type: "date", Value: "0601"}, {Name: "serial", Type: "serial", Length: 4}, }, } gen := NewGenerator(map[string]*schema.Schema{"date-custom": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "date-custom", Values: map[string]string{}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } // Format "0601" produces YYMM if matched, _ := regexp.MatchString(`^\d{4}-\d{4}$`, pn); !matched { t.Errorf("got %q, want pattern YYMM-NNNN", pn) } want := time.Now().UTC().Format("0601") + "-0001" if pn != want { t.Errorf("got %q, want %q", pn, want) } } // --- Validation tests --- func TestValidateBasic(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) if err := gen.Validate("F01-0001", "test"); err != nil { t.Fatalf("expected valid, got error: %v", err) } } func TestValidateWrongSegmentCount(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) if err := gen.Validate("F01-0001-EXTRA", "test"); err == nil { t.Fatal("expected error for wrong segment count") } } func TestValidateInvalidEnum(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) if err := gen.Validate("ZZZ-0001", "test"); err == nil { t.Fatal("expected error for invalid enum value") } } func TestValidateNonNumericSerial(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) if err := gen.Validate("F01-ABCD", "test"); err == nil { t.Fatal("expected error for non-numeric serial") } } func TestValidateSerialWrongLength(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) if err := gen.Validate("F01-01", "test"); err == nil { t.Fatal("expected error for wrong serial length") } } func TestValidateConstantSegment(t *testing.T) { s := &schema.Schema{ Name: "const-val", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "prefix", Type: "constant", Value: "KS"}, {Name: "serial", Type: "serial", Length: 4}, }, } gen := NewGenerator(map[string]*schema.Schema{"const-val": s}, &mockSeqStore{}) if err := gen.Validate("KS-0001", "const-val"); err != nil { t.Fatalf("expected valid, got error: %v", err) } if err := gen.Validate("XX-0001", "const-val"); err == nil { t.Fatal("expected error for wrong constant value") } } func TestValidateUnknownSchema(t *testing.T) { gen := NewGenerator(map[string]*schema.Schema{}, &mockSeqStore{}) if err := gen.Validate("F01-0001", "nonexistent"); err == nil { t.Fatal("expected error for unknown schema") } } func TestValidateDateSegment(t *testing.T) { s := &schema.Schema{ Name: "date-val", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "date", Type: "date"}, {Name: "serial", Type: "serial", Length: 3}, }, } gen := NewGenerator(map[string]*schema.Schema{"date-val": s}, &mockSeqStore{}) today := time.Now().UTC().Format("20060102") if err := gen.Validate(today+"-001", "date-val"); err != nil { t.Fatalf("expected valid, got error: %v", err) } if err := gen.Validate("20-001", "date-val"); err == nil { t.Fatal("expected error for wrong date length") } } func TestValidateGeneratedOutput(t *testing.T) { s := testSchema() gen := NewGenerator(map[string]*schema.Schema{"test": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "test", Values: map[string]string{"category": "F01"}, }) if err != nil { t.Fatalf("Generate error: %v", err) } if err := gen.Validate(pn, "test"); err != nil { t.Fatalf("generated part number %q failed validation: %v", pn, err) } } func TestGenerateDateSegmentYearOnly(t *testing.T) { s := &schema.Schema{ Name: "date-year", Version: 1, Separator: "-", Segments: []schema.Segment{ {Name: "year", Type: "date", Value: "2006"}, {Name: "serial", Type: "serial", Length: 4}, }, } gen := NewGenerator(map[string]*schema.Schema{"date-year": s}, &mockSeqStore{}) pn, err := gen.Generate(context.Background(), Input{ SchemaName: "date-year", Values: map[string]string{}, }) if err != nil { t.Fatalf("Generate returned error: %v", err) } want := time.Now().UTC().Format("2006") + "-0001" if pn != want { t.Errorf("got %q, want %q", pn, want) } }