package classes import ( "crypto/sha256" "encoding/hex" "regexp" "strings" "testing" ) // TestDeviceCodes pins the device token and short code (D-11, D-06) // without a database: 10,000 tokens are 64 base64url characters, all // distinct, and their stored form is the SHA-256 hex; 10,000 short codes // are XXXX-XXXX over DeviceShortCodeAlphabet with every character of the // alphabet in every position and no character far from its expected // share; NormalizeDeviceShortCode accepts case, spaces and a missing // hyphen and refuses lengths other than eight and the excluded letters. func TestDeviceCodes(t *testing.T) { const draws = 10000 t.Run("tokens", func(t *testing.T) { seen := make(map[string]struct{}, draws) for range draws { tok, err := NewDeviceToken() if err != nil { t.Fatal(err) } if len(tok) != 64 { t.Fatalf("token %q has %d characters", tok, len(tok)) } if strings.Trim(tok, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") != "" { t.Fatalf("token %q is not base64url", tok) } if _, dup := seen[tok]; dup { t.Fatalf("token %q drawn twice", tok) } seen[tok] = struct{}{} } tok, _ := NewDeviceToken() sum := sha256.Sum256([]byte(tok)) if got := HashAPIToken(tok); got != hex.EncodeToString(sum[:]) || len(got) != 64 || got == tok { t.Fatalf("stored form %q is not the SHA-256 hex of the token", got) } }) t.Run("short_codes", func(t *testing.T) { alphabet := DeviceShortCodeAlphabet if alphabet != "23456789ABCDEFGHJKMNPQRSTUVWXYZ" || len(alphabet) != 31 { t.Fatalf("alphabet = %q", alphabet) } format := regexp.MustCompile(`^[` + alphabet + `]{4}-[` + alphabet + `]{4}$`) perPosition := make([]map[byte]int, 8) for i := range perPosition { perPosition[i] = map[byte]int{} } total := map[byte]int{} seen := map[string]struct{}{} for range draws { code, err := NewDeviceShortCode() if err != nil { t.Fatal(err) } if !format.MatchString(code) { t.Fatalf("short code %q", code) } seen[code] = struct{}{} raw := code[:4] + code[5:] for i := 0; i < 8; i++ { perPosition[i][raw[i]]++ total[raw[i]]++ } } // 31^8 codes: 10,000 draws repeating more than a handful would mean // a broken generator. if len(seen) < draws-5 { t.Fatalf("%d distinct codes in %d draws", len(seen), draws) } for i, counts := range perPosition { for j := 0; j < len(alphabet); j++ { if counts[alphabet[j]] == 0 { t.Fatalf("character %q never drawn at position %d", alphabet[j], i) } } } // Each character is expected 80000/31 ≈ 2581 times; a modulo bias // of the byte draw would push the first 8 characters ~3% up. Allow // 10% either way, far outside the noise of a fair draw. expected := float64(8*draws) / float64(len(alphabet)) for j := 0; j < len(alphabet); j++ { got := float64(total[alphabet[j]]) if got < expected*0.9 || got > expected*1.1 { t.Fatalf("character %q drawn %v times, expected about %.0f", alphabet[j], got, expected) } } for _, excluded := range "01ILO" { if total[byte(excluded)] != 0 { t.Fatalf("excluded character %q drawn", excluded) } } }) t.Run("normalization", func(t *testing.T) { for _, c := range []struct{ in, want string }{ {"ABCD-EFGH", "ABCD-EFGH"}, {"abcd-efgh", "ABCD-EFGH"}, {"AbCd-eFgH", "ABCD-EFGH"}, {"ABCDEFGH", "ABCD-EFGH"}, {"abcdefgh", "ABCD-EFGH"}, {" ab cd ef gh ", "ABCD-EFGH"}, {"ABCD - EFGH", "ABCD-EFGH"}, {"A-B-C-D-E-F-G-H", "ABCD-EFGH"}, {"2345-6789", "2345-6789"}, {"zzzz zzzz", "ZZZZ-ZZZZ"}, } { if got, ok := NormalizeDeviceShortCode(c.in); !ok || got != c.want { t.Errorf("normalize %q = %q, %v; want %q", c.in, got, ok, c.want) } } for _, in := range []string{ "", "-", "ABCDEFG", "ABCD-EFG", "ABCDEFGHJ", "ABCD-EFGHJ", "ABCDEFG0", "ABCDEFG1", "ABCDEFGI", "ABCDEFGL", "ABCDEFGO", "abcdefgo", "abcdefgl", "abcdefgi", "ABCD_EFGH", "ABCD.EFGH", "ABCDÉFGH", "ABCD\tEFGH", } { if got, ok := NormalizeDeviceShortCode(in); ok { t.Errorf("normalize %q accepted as %q", in, got) } } }) }