Feat v0.9.5 typed forms sdk (#79)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 3m6s
CI/CD / build-and-deploy (push) Successful in 29s
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 3m6s
CI/CD / build-and-deploy (push) Successful in 29s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #79.
This commit is contained in:
322
server/forms/forms_test.go
Normal file
322
server/forms/forms_test.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── Parse Tests ──────────────────────────────
|
||||
|
||||
func TestParseTypedFormTemplate_Empty(t *testing.T) {
|
||||
for _, input := range []string{"", "{}", "null"} {
|
||||
if tpl := ParseTypedFormTemplate(json.RawMessage(input)); tpl != nil {
|
||||
t.Errorf("expected nil for %q, got %+v", input, tpl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypedFormTemplate_InvalidJSON(t *testing.T) {
|
||||
if tpl := ParseTypedFormTemplate(json.RawMessage(`{bad`)); tpl != nil {
|
||||
t.Fatal("expected nil for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypedFormTemplate_FlatFields(t *testing.T) {
|
||||
raw := json.RawMessage(`{
|
||||
"fields": [
|
||||
{"key": "name", "type": "text", "label": "Name"},
|
||||
{"key": "email", "type": "email", "label": "Email"}
|
||||
]
|
||||
}`)
|
||||
tpl := ParseTypedFormTemplate(raw)
|
||||
if tpl == nil {
|
||||
t.Fatal("expected non-nil template")
|
||||
}
|
||||
if len(tpl.Fields) != 2 {
|
||||
t.Fatalf("expected 2 fields, got %d", len(tpl.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypedFormTemplate_Fieldsets(t *testing.T) {
|
||||
raw := json.RawMessage(`{
|
||||
"fieldsets": [
|
||||
{"label": "Step 1", "fields": [{"key": "a", "type": "text", "label": "A"}]},
|
||||
{"label": "Step 2", "fields": [{"key": "b", "type": "number", "label": "B"}]}
|
||||
]
|
||||
}`)
|
||||
tpl := ParseTypedFormTemplate(raw)
|
||||
if tpl == nil {
|
||||
t.Fatal("expected non-nil template")
|
||||
}
|
||||
if len(tpl.Fields) != 2 {
|
||||
t.Fatalf("expected 2 flattened fields, got %d", len(tpl.Fields))
|
||||
}
|
||||
if len(tpl.Fieldsets) != 2 {
|
||||
t.Fatalf("expected 2 fieldsets, got %d", len(tpl.Fieldsets))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypedFormTemplate_LegacyFormat(t *testing.T) {
|
||||
// Legacy format: fields present but missing key/type → returns nil
|
||||
raw := json.RawMessage(`{"fields": [{"label": "Name"}]}`)
|
||||
if tpl := ParseTypedFormTemplate(raw); tpl != nil {
|
||||
t.Fatal("expected nil for legacy format")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validation Tests ─────────────────────────
|
||||
|
||||
func tpl(fields ...FormField) *TypedFormTemplate {
|
||||
return &TypedFormTemplate{Fields: fields}
|
||||
}
|
||||
|
||||
func intPtr(n int) *int { return &n }
|
||||
func floatPtr(n float64) *float64 { return &n }
|
||||
|
||||
func TestValidateFormData_Required(t *testing.T) {
|
||||
template := tpl(FormField{Key: "name", Type: "text", Label: "Name", Required: true})
|
||||
|
||||
// Missing
|
||||
errs := ValidateFormData(template, map[string]interface{}{})
|
||||
if len(errs) != 1 || errs[0].Key != "name" {
|
||||
t.Fatalf("expected 1 error for missing required field, got %+v", errs)
|
||||
}
|
||||
|
||||
// Present
|
||||
errs = ValidateFormData(template, map[string]interface{}{"name": "Alice"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
|
||||
// Empty string counts as missing
|
||||
errs = ValidateFormData(template, map[string]interface{}{"name": ""})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected 1 error for empty required field, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Text(t *testing.T) {
|
||||
template := tpl(FormField{
|
||||
Key: "bio", Type: "text", Label: "Bio",
|
||||
Validation: &FormValidation{MinLength: intPtr(3), MaxLength: intPtr(10), Pattern: `^[a-z]+$`},
|
||||
})
|
||||
|
||||
// Too short
|
||||
errs := ValidateFormData(template, map[string]interface{}{"bio": "ab"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected min_length error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Too long
|
||||
errs = ValidateFormData(template, map[string]interface{}{"bio": "abcdefghijk"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected max_length error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Pattern mismatch
|
||||
errs = ValidateFormData(template, map[string]interface{}{"bio": "ABC"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected pattern error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Valid
|
||||
errs = ValidateFormData(template, map[string]interface{}{"bio": "hello"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Email(t *testing.T) {
|
||||
template := tpl(FormField{Key: "email", Type: "email", Label: "Email"})
|
||||
|
||||
errs := ValidateFormData(template, map[string]interface{}{"email": "bad"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected email error, got %+v", errs)
|
||||
}
|
||||
|
||||
errs = ValidateFormData(template, map[string]interface{}{"email": "a@b.c"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Number(t *testing.T) {
|
||||
template := tpl(FormField{
|
||||
Key: "age", Type: "number", Label: "Age",
|
||||
Validation: &FormValidation{Min: floatPtr(0), Max: floatPtr(150)},
|
||||
})
|
||||
|
||||
// Below min
|
||||
errs := ValidateFormData(template, map[string]interface{}{"age": float64(-1)})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected min error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Above max
|
||||
errs = ValidateFormData(template, map[string]interface{}{"age": float64(200)})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected max error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Non-number
|
||||
errs = ValidateFormData(template, map[string]interface{}{"age": "thirty"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected type error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Valid
|
||||
errs = ValidateFormData(template, map[string]interface{}{"age": float64(25)})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Date(t *testing.T) {
|
||||
template := tpl(FormField{
|
||||
Key: "start", Type: "date", Label: "Start",
|
||||
Validation: &FormValidation{MinDate: "2024-01-01", MaxDate: "2024-12-31"},
|
||||
})
|
||||
|
||||
// Invalid format
|
||||
errs := ValidateFormData(template, map[string]interface{}{"start": "Jan 1"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected format error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Before min
|
||||
errs = ValidateFormData(template, map[string]interface{}{"start": "2023-12-31"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected min_date error, got %+v", errs)
|
||||
}
|
||||
|
||||
// After max
|
||||
errs = ValidateFormData(template, map[string]interface{}{"start": "2025-01-01"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected max_date error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Valid
|
||||
errs = ValidateFormData(template, map[string]interface{}{"start": "2024-06-15"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Select(t *testing.T) {
|
||||
template := tpl(FormField{
|
||||
Key: "color", Type: "select", Label: "Color",
|
||||
Options: []FormOption{{Value: "red", Label: "Red"}, {Value: "blue", Label: "Blue"}},
|
||||
})
|
||||
|
||||
errs := ValidateFormData(template, map[string]interface{}{"color": "green"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected invalid option error, got %+v", errs)
|
||||
}
|
||||
|
||||
// Case-insensitive match
|
||||
errs = ValidateFormData(template, map[string]interface{}{"color": "Red"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors (case-insensitive), got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Checkbox(t *testing.T) {
|
||||
template := tpl(FormField{Key: "agree", Type: "checkbox", Label: "Agree"})
|
||||
|
||||
errs := ValidateFormData(template, map[string]interface{}{"agree": "yes"})
|
||||
if len(errs) != 1 {
|
||||
t.Fatalf("expected type error, got %+v", errs)
|
||||
}
|
||||
|
||||
errs = ValidateFormData(template, map[string]interface{}{"agree": true})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_Condition(t *testing.T) {
|
||||
template := tpl(
|
||||
FormField{Key: "type", Type: "select", Label: "Type", Options: []FormOption{{Value: "normal", Label: "Normal"}, {Value: "other", Label: "Other"}}},
|
||||
FormField{
|
||||
Key: "details", Type: "text", Label: "Details", Required: true,
|
||||
Condition: &FieldCondition{When: "type", Op: "eq", Value: "other"},
|
||||
},
|
||||
)
|
||||
|
||||
// Condition not met → required field skipped
|
||||
errs := ValidateFormData(template, map[string]interface{}{"type": "normal"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors (condition not met), got %+v", errs)
|
||||
}
|
||||
|
||||
// Condition met → required enforced
|
||||
errs = ValidateFormData(template, map[string]interface{}{"type": "other"})
|
||||
if len(errs) != 1 || errs[0].Key != "details" {
|
||||
t.Fatalf("expected 1 error for conditional required field, got %+v", errs)
|
||||
}
|
||||
|
||||
// Condition met + value provided
|
||||
errs = ValidateFormData(template, map[string]interface{}{"type": "other", "details": "More info"})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors, got %+v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateFieldCondition_Operators(t *testing.T) {
|
||||
data := map[string]interface{}{"status": "active", "role": "admin"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cond FieldCondition
|
||||
expect bool
|
||||
}{
|
||||
{"eq match", FieldCondition{When: "status", Op: "eq", Value: "active"}, true},
|
||||
{"eq mismatch", FieldCondition{When: "status", Op: "eq", Value: "inactive"}, false},
|
||||
{"neq match", FieldCondition{When: "status", Op: "neq", Value: "inactive"}, true},
|
||||
{"neq mismatch", FieldCondition{When: "status", Op: "neq", Value: "active"}, false},
|
||||
{"exists present", FieldCondition{When: "status", Op: "exists"}, true},
|
||||
{"exists absent", FieldCondition{When: "missing", Op: "exists"}, false},
|
||||
{"in match", FieldCondition{When: "role", Op: "in", Value: []interface{}{"admin", "mod"}}, true},
|
||||
{"in mismatch", FieldCondition{When: "role", Op: "in", Value: []interface{}{"user", "guest"}}, false},
|
||||
{"default op is eq", FieldCondition{When: "status", Value: "active"}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := EvaluateFieldCondition(&tt.cond, data)
|
||||
if got != tt.expect {
|
||||
t.Errorf("expected %v, got %v", tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllFields(t *testing.T) {
|
||||
raw := json.RawMessage(`{
|
||||
"fieldsets": [
|
||||
{"label": "A", "fields": [{"key": "x", "type": "text", "label": "X"}]},
|
||||
{"label": "B", "fields": [{"key": "y", "type": "text", "label": "Y"}, {"key": "z", "type": "text", "label": "Z"}]}
|
||||
]
|
||||
}`)
|
||||
tpl := ParseTypedFormTemplate(raw)
|
||||
if tpl == nil {
|
||||
t.Fatal("expected non-nil template")
|
||||
}
|
||||
all := tpl.AllFields()
|
||||
if len(all) != 3 {
|
||||
t.Fatalf("expected 3 fields, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFormData_OptionalSkipped(t *testing.T) {
|
||||
template := tpl(FormField{
|
||||
Key: "note", Type: "text", Label: "Note",
|
||||
Validation: &FormValidation{MinLength: intPtr(5)},
|
||||
})
|
||||
|
||||
// Not present and not required → no error
|
||||
errs := ValidateFormData(template, map[string]interface{}{})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("expected 0 errors for absent optional field, got %+v", errs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user