✨ feat(user): magic_link_tokens table + repository (ADR-0028 Phase A.3) (#61)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #61.
This commit is contained in:
78
pkg/user/magic_link_test.go
Normal file
78
pkg/user/magic_link_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGenerateMagicLinkToken_ShapeAndHashAgree confirms the contract that
|
||||
// HashMagicLinkToken(plaintext) == returned hashHex. Without that, the
|
||||
// consume handler can never look up what request stored.
|
||||
func TestGenerateMagicLinkToken_ShapeAndHashAgree(t *testing.T) {
|
||||
plain, hashHex, err := GenerateMagicLinkToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, plain)
|
||||
assert.NotEmpty(t, hashHex)
|
||||
assert.Len(t, hashHex, 64, "sha256 hex = 64 chars")
|
||||
assert.Equal(t, hashHex, HashMagicLinkToken(plain),
|
||||
"GenerateMagicLinkToken must return a hash that matches HashMagicLinkToken(plain)")
|
||||
}
|
||||
|
||||
// TestGenerateMagicLinkToken_PlainIsURLSafeBase64 confirms the link can
|
||||
// be embedded in a URL without further escaping. RawURLEncoding => no
|
||||
// "/", "+", or "=" padding chars.
|
||||
func TestGenerateMagicLinkToken_PlainIsURLSafeBase64(t *testing.T) {
|
||||
plain, _, err := GenerateMagicLinkToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, bad := range []string{"/", "+", "="} {
|
||||
assert.False(t, strings.Contains(plain, bad),
|
||||
"plaintext token must not contain %q (URL-unsafe)", bad)
|
||||
}
|
||||
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(plain)
|
||||
require.NoError(t, err, "plaintext must round-trip through RawURLEncoding")
|
||||
assert.Len(t, decoded, 32, "32 bytes of entropy")
|
||||
}
|
||||
|
||||
// TestGenerateMagicLinkToken_Unique confirms two consecutive calls
|
||||
// produce different tokens (not a deterministic seeding bug).
|
||||
func TestGenerateMagicLinkToken_Unique(t *testing.T) {
|
||||
a, ah, err := GenerateMagicLinkToken()
|
||||
require.NoError(t, err)
|
||||
b, bh, err := GenerateMagicLinkToken()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, a, b, "plaintexts must differ between calls")
|
||||
assert.NotEqual(t, ah, bh, "hashes must differ between calls")
|
||||
}
|
||||
|
||||
// TestHashMagicLinkToken_StableAndCorrect confirms HashMagicLinkToken is
|
||||
// a pure function (same input -> same output) AND that it produces the
|
||||
// expected sha256 hex digest. Cross-checked against the stdlib so we
|
||||
// catch any accidental algorithm swap.
|
||||
func TestHashMagicLinkToken_StableAndCorrect(t *testing.T) {
|
||||
const sample = "abc123-test-token"
|
||||
got1 := HashMagicLinkToken(sample)
|
||||
got2 := HashMagicLinkToken(sample)
|
||||
assert.Equal(t, got1, got2, "HashMagicLinkToken must be deterministic")
|
||||
|
||||
sum := sha256.Sum256([]byte(sample))
|
||||
want := hex.EncodeToString(sum[:])
|
||||
assert.Equal(t, want, got1, "HashMagicLinkToken must be sha256 hex")
|
||||
}
|
||||
|
||||
// TestHashMagicLinkToken_DiffersOnDifferentInput is the tautological
|
||||
// counter-test of stability : different inputs -> different outputs.
|
||||
// Catches the (unlikely) case where someone replaces the impl with
|
||||
// a constant.
|
||||
func TestHashMagicLinkToken_DiffersOnDifferentInput(t *testing.T) {
|
||||
assert.NotEqual(t, HashMagicLinkToken("a"), HashMagicLinkToken("b"))
|
||||
}
|
||||
Reference in New Issue
Block a user