Implement Generator.Validate() to check part numbers against schemas: - Split by separator, verify segment count - Constant: must equal expected value - Enum: must be in allowed values map - String: length, case, pattern constraints - Serial: length + numeric-only check - Date: length matches expected format output Add belt-and-suspenders call in HandleCreateItem after Generate(). Add 9 validation tests (all pass alongside 10 existing tests). Closes #80
366 lines
9.2 KiB
Go
366 lines
9.2 KiB
Go
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)
|
|
}
|
|
}
|