auth-ui: rewrite ui

Change-Id: I6f00867015ec77aa7e336e89da4dc1b081e330c6
diff --git a/core/auth/ui/validation_test.go b/core/auth/ui/validation_test.go
new file mode 100644
index 0000000..437376b
--- /dev/null
+++ b/core/auth/ui/validation_test.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+)
+
+const (
+	testUsernameLengthMessage      = "Username must be at least 3 characters long."
+	testPasswordLengthMessage      = "Password must be at least 20 characters long."
+	testPasswordCompositionMessage = "Password must contain at least one digit, lower&upper case and special characters"
+)
+
+func TestValidateUsernameBoundaries(t *testing.T) {
+	tests := []struct {
+		name     string
+		username string
+		want     []ValidationError
+	}{
+		{"empty", "", []ValidationError{{Field: "username", Message: testUsernameLengthMessage}}},
+		{"two ASCII bytes", "ab", []ValidationError{{Field: "username", Message: testUsernameLengthMessage}}},
+		{"three ASCII bytes", "abc", nil},
+		{"existing byte semantics", "é", []ValidationError{{Field: "username", Message: testUsernameLengthMessage}}},
+		{"four UTF-8 bytes", "éé", nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := validateUsername(tt.username); !reflect.DeepEqual(got, tt.want) {
+				t.Fatalf("validateUsername() = %#v, want %#v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestValidatePasswordLengthAndDeterministicOrdering(t *testing.T) {
+	if got := validatePassword("short"); !reflect.DeepEqual(got, []ValidationError{
+		{Field: "password", Message: testPasswordLengthMessage},
+		{Field: "password", Message: testPasswordCompositionMessage},
+	}) {
+		t.Fatalf("short password errors = %#v", got)
+	}
+	if got := validatePassword("Aa1!" + strings.Repeat("x", 15)); len(got) != 1 || got[0].Message != testPasswordLengthMessage {
+		t.Fatalf("19-byte password errors = %#v", got)
+	}
+	if got := validatePassword("Aa1!" + strings.Repeat("x", 16)); len(got) != 0 {
+		t.Fatalf("20-byte password errors = %#v", got)
+	}
+}
+
+func TestValidatePasswordUnicodeCategories(t *testing.T) {
+	valid := "Éé١ " + strings.Repeat("x", 16)
+	if got := validatePassword(valid); len(got) != 0 {
+		t.Fatalf("Unicode category password errors = %#v", got)
+	}
+}
+
+func TestValidatePasswordAcceptsEveryConfiguredASCIISpecial(t *testing.T) {
+	for _, special := range " !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~" {
+		t.Run(string(special), func(t *testing.T) {
+			password := "Aa1" + strings.Repeat("x", 16) + string(special)
+			if got := validatePassword(password); len(got) != 0 {
+				t.Fatalf("configured special %q rejected: %#v", special, got)
+			}
+		})
+	}
+}
+
+func TestValidatePasswordRejectsUnconfiguredPunctuation(t *testing.T) {
+	password := "Aa1" + strings.Repeat("x", 16) + "§"
+	got := validatePassword(password)
+	if !reflect.DeepEqual(got, []ValidationError{{Field: "password", Message: testPasswordCompositionMessage}}) {
+		t.Fatalf("unconfigured punctuation errors = %#v", got)
+	}
+}