From 7c85ff1ed67fde063b9015f5be9b0562d811e09c Mon Sep 17 00:00:00 2001 From: jackspirou Date: Sun, 12 Jan 2025 00:55:19 -0600 Subject: [PATCH] update tests --- README.md | 4 +- apikey.go | 34 +++++++++++--- apikey_test.go | 124 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 26260f5..cda13e4 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ func NewAPIKey(prefix, uuid string, opts ...Option) APIKey NewAPIKey creates a new APIKey from a string prefix, string UUID, and options. -### func [NewAPIKeyFromBytes]() +### func [NewAPIKeyFromBytes]() ```go func NewAPIKeyFromBytes(prefix string, uuid [16]byte, opts ...Option) APIKey @@ -271,7 +271,7 @@ func NewAPIKeyFromBytes(prefix string, uuid [16]byte, opts ...Option) APIKey NewAPIKeyFromBytes creates a new APIKey from a string prefix, \[16\]byte UUID, and options. -### func [ParseAPIKey]() +### func [ParseAPIKey]() ```go func ParseAPIKey(apikey string) (APIKey, error) diff --git a/apikey.go b/apikey.go index b3011e6..6a03950 100644 --- a/apikey.go +++ b/apikey.go @@ -83,9 +83,9 @@ func (a APIKey) calculateChecksum() string { // Combine all parts except checksum data := a.Prefix + "_" + a.Key.String() + a.Entropy - // Calculate CRC32 checksum and return as hex + // Calculate CRC32 checksum and return as uppercase hex crc := crc32.ChecksumIEEE([]byte(data)) - return fmt.Sprintf("%08x", crc) // Always 8 chars, zero-padded + return fmt.Sprintf("%08X", crc) // Always 8 chars, zero-padded, uppercase } // String returns the complete API key as a string with all components joined @@ -98,6 +98,10 @@ func (a APIKey) String() string { // NewAPIKey creates a new APIKey from a string prefix, string UUID, and options. func NewAPIKey(prefix, uuid string, opts ...Option) APIKey { + if prefix == "" { + panic("prefix cannot be empty") + } + // apply the options to the default configuration options := apply(opts...) @@ -219,13 +223,13 @@ func ParseAPIKey(apikey string) (APIKey, error) { } prefix := parts[0] + if prefix == "" { + return APIKey{}, fmt.Errorf("invalid prefix: cannot be empty") + } + remainder := parts[1] checksum := parts[2] - if len(checksum) != checksumLength { - return APIKey{}, fmt.Errorf("invalid checksum length: expected %d characters, got %d", checksumLength, len(checksum)) - } - // The remainder should contain both the Key and Entropy parts if len(remainder) < KeyLengthWithoutHyphens { return APIKey{}, fmt.Errorf("invalid Key format: insufficient length") @@ -233,6 +237,14 @@ func ParseAPIKey(apikey string) (APIKey, error) { // Extract the Key part (first KeyLengthWithoutHyphens characters) keyPart := remainder[:KeyLengthWithoutHyphens] + + // Validate key format first + for _, c := range keyPart { + if !strings.ContainsRune("0123456789ABCDEFGHJKMNPQRSTVWXYZ", c) { + return APIKey{}, fmt.Errorf("invalid Key format: contains invalid characters") + } + } + key, err := Parse(keyPart) if err != nil { return APIKey{}, fmt.Errorf("invalid Key format: %v", err) @@ -241,6 +253,16 @@ func ParseAPIKey(apikey string) (APIKey, error) { // The rest is entropy entropy := remainder[KeyLengthWithoutHyphens:] + // Validate checksum format (must be 8 uppercase hexadecimal characters) + if len(checksum) != checksumLength { + return APIKey{}, fmt.Errorf("invalid checksum format: must be 8 hexadecimal characters") + } + for _, c := range checksum { + if !strings.ContainsRune("0123456789ABCDEF", c) { + return APIKey{}, fmt.Errorf("invalid checksum format: must be 8 hexadecimal characters") + } + } + apiKey := APIKey{ Prefix: prefix, Key: key, diff --git a/apikey_test.go b/apikey_test.go index 9d6c3eb..310b575 100644 --- a/apikey_test.go +++ b/apikey_test.go @@ -53,20 +53,58 @@ func TestAPIKey(t *testing.T) { } }, }, + { + name: "Empty prefix", + prefix: "", + uuid: "d1756360-5da0-40df-9926-a76abff5601d", + wantErr: true, + validate: func(t *testing.T, key APIKey) { + // We shouldn't reach this point if wantErr is true + t.Error("expected error for empty prefix, got none") + }, + }, + { + name: "Multiple options", + prefix: "TEST", + uuid: "d1756360-5da0-40df-9926-a76abff5601d", + options: []Option{With128BitEntropy, With256BitEntropy}, // Last option should win + wantLen: 42, + validate: func(t *testing.T, key APIKey) { + if len(key.Entropy) != 42 { + t.Errorf("wrong entropy length, got %d, want 42", len(key.Entropy)) + } + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Catch panics and convert to errors if we expect an error + defer func() { + if r := recover(); r != nil { + if !tt.wantErr { + t.Errorf("unexpected panic: %v", r) + } + } + }() + // Test NewAPIKey key := NewAPIKey(tt.prefix, tt.uuid, tt.options...) + if tt.wantErr { + t.Error("expected error, got none") + return + } + // Validate entropy length if len(key.Entropy) != tt.wantLen { t.Errorf("entropy length mismatch, got %d, want %d", len(key.Entropy), tt.wantLen) } - // Run additional validations - tt.validate(t, key) + // Run additional validations if provided + if tt.validate != nil { + tt.validate(t, key) + } // Test String() method str := key.String() @@ -141,6 +179,23 @@ func TestNewAPIKeyFromBytes(t *testing.T) { } }, }, + { + name: "256-bit entropy", + prefix: "PROD", + options: []Option{With256BitEntropy}, + wantLen: 42, + validate: func(t *testing.T, key APIKey) { + if len(key.Entropy) != 42 { + t.Errorf("wrong entropy length, got %d, want 42", len(key.Entropy)) + } + // Validate entropy characters are in valid Base32-Crockford range + for _, c := range key.Entropy { + if !strings.ContainsRune("0123456789ABCDEFGHJKMNPQRSTVWXYZ", c) { + t.Errorf("invalid character in entropy: %c", c) + } + } + }, + }, } for _, tt := range tests { @@ -198,14 +253,25 @@ func TestParseAPIKeyErrors(t *testing.T) { wantErr: "invalid APIKey format: expected 3 parts, got 4", }, { - name: "Invalid key format", + name: "Invalid prefix", + input: "_38QARV01ET0G6Z2CJD9VA2ZZAR0X_12345678", + wantErr: "invalid prefix: cannot be empty", + }, + { + name: "Invalid key length", input: "TEST_INVALID_12345678", wantErr: "invalid Key format: insufficient length", }, + { + name: "Invalid characters in key", + // Using proper length but invalid characters + input: "TEST_38QARV01ET0G6Z2CJD9VA2ZZ!R0XAAAAAA_12345678", + wantErr: "invalid Key format: contains invalid characters", + }, { name: "Invalid checksum length", input: "TEST_38QARV01ET0G6Z2CJD9VA2ZZAR0X_123", - wantErr: "invalid checksum length: expected 8 characters, got 3", + wantErr: "invalid checksum format: must be 8 hexadecimal characters", }, { name: "Invalid checksum value", @@ -226,3 +292,53 @@ func TestParseAPIKeyErrors(t *testing.T) { }) } } + +func TestEntropyCharacterValidation(t *testing.T) { + tests := []struct { + name string + prefix string + options []Option + }{ + { + name: "128-bit character validation", + prefix: "TEST", + options: []Option{With128BitEntropy}, + }, + { + name: "160-bit (default) character validation", + prefix: "TEST", + options: []Option{}, + }, + { + name: "256-bit character validation", + prefix: "TEST", + options: []Option{With256BitEntropy}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := NewAPIKey(tt.prefix, "d1756360-5da0-40df-9926-a76abff5601d", tt.options...) + + // Validate all characters are in Base32-Crockford alphabet + for _, c := range key.Entropy { + if !strings.ContainsRune("0123456789ABCDEFGHJKMNPQRSTVWXYZ", c) { + t.Errorf("invalid character in entropy: %c", c) + } + } + + // Validate checksum is valid uppercase hexadecimal (CRC32) + str := key.String() + parts := strings.Split(str, "_") + checksum := parts[2] + if len(checksum) != 8 { + t.Errorf("checksum length must be 8, got %d", len(checksum)) + } + for _, c := range checksum { + if !strings.ContainsRune("0123456789ABCDEF", c) { + t.Errorf("invalid character in checksum: %c (must be uppercase hexadecimal)", c) + } + } + }) + } +}