Files
realy/auth/nip42_test.go
2025-06-26 15:00:56 +01:00

326 lines
8.8 KiB
Go

package auth
import (
"bytes"
"encoding/base64"
"net/url"
"testing"
"time"
"realy.lol/chk"
"realy.lol/event"
"realy.lol/kind"
"realy.lol/p256k"
"realy.lol/tag"
"realy.lol/tags"
"realy.lol/timestamp"
)
// TestGenerateChallenge tests that GenerateChallenge produces valid base64 strings
// of the expected length and that they are unique.
func TestGenerateChallenge(t *testing.T) {
// Test that challenges are of expected length
challenge := GenerateChallenge()
if len(challenge) != 16 {
t.Errorf("Expected challenge length to be 16, got %d", len(challenge))
}
// Test that challenges are valid base64
_, err := base64.StdEncoding.DecodeString(string(challenge))
if err != nil {
t.Errorf("Challenge is not valid base64: %v", err)
}
// Test that challenges are unique
challenges := make(map[string]bool)
for i := 0; i < 100; i++ {
challenge := GenerateChallenge()
challengeStr := string(challenge)
if challenges[challengeStr] {
t.Errorf("Generated duplicate challenge: %s", challengeStr)
}
challenges[challengeStr] = true
}
}
// TestCreateUnsigned tests that CreateUnsigned creates events with the correct properties.
func TestCreateUnsigned(t *testing.T) {
var err error
signer := new(p256k.Signer)
if err = signer.Generate(); chk.E(err) {
t.Fatal(err)
}
challenge := GenerateChallenge()
const relayURL = "wss://example.com"
ev := CreateUnsigned(signer.Pub(), challenge, relayURL)
// Check event properties
if !bytes.Equal(ev.Pubkey, signer.Pub()) {
t.Errorf("Event pubkey doesn't match: expected %x, got %x", signer.Pub(), ev.Pubkey)
}
if ev.Kind != kind.ClientAuthentication {
t.Errorf("Event kind doesn't match: expected %v, got %v", kind.ClientAuthentication, ev.Kind)
}
// Check relay tag
relayTag := ev.Tags.GetFirst(tag.New("relay", ""))
if relayTag == nil {
t.Error("Relay tag is missing")
} else if string(relayTag.Value()) != relayURL {
t.Errorf("Relay tag value doesn't match: expected %s, got %s", relayURL, relayTag.Value())
}
// Check challenge tag
challengeTag := ev.Tags.GetFirst(tag.New("challenge", ""))
if challengeTag == nil {
t.Error("Challenge tag is missing")
} else if string(challengeTag.Value()) != string(challenge) {
t.Errorf("Challenge tag value doesn't match: expected %s, got %s", challenge, challengeTag.Value())
}
}
// TestParseURL tests the parseURL helper function with various inputs.
func TestParseURL(t *testing.T) {
// This test explicitly uses the url.URL type from the net/url package
var _ *url.URL // Ensure url package is used
tests := []struct {
input string
expected string
wantErr bool
}{
{"https://example.com", "https://example.com", false},
{"https://example.com/", "https://example.com", false},
{"HTTPS://EXAMPLE.COM/", "https://example.com", false},
{"https://example.com/path", "https://example.com/path", false},
{"https://example.com/path/", "https://example.com/path", false},
{"https://user:pass@example.com", "https://user:pass@example.com", false},
{"https://example.com:8080", "https://example.com:8080", false},
{"wss://example.com", "wss://example.com", false},
{"://invalid", "", true},
}
for _, tt := range tests {
u, err := parseURL(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseURL(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
continue
}
if err == nil {
got := u.String()
if got != tt.expected {
t.Errorf("parseURL(%q) = %q, want %q", tt.input, got, tt.expected)
}
}
}
}
// TestValidate tests the Validate function with various scenarios.
func TestValidate(t *testing.T) {
var err error
signer := new(p256k.Signer)
if err = signer.Generate(); chk.E(err) {
t.Fatal(err)
}
challenge := GenerateChallenge()
const relayURL = "wss://example.com"
// Helper to create a valid event
createValidEvent := func() *event.T {
ev := CreateUnsigned(signer.Pub(), challenge, relayURL)
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
return ev
}
// Test valid event
t.Run("Valid event", func(t *testing.T) {
ev := createValidEvent()
ok, err := Validate(ev, challenge, relayURL)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !ok {
t.Error("Expected validation to pass")
}
})
// Test invalid kind
t.Run("Invalid kind", func(t *testing.T) {
ev := createValidEvent()
ev.Kind = kind.TextNote // Change to an invalid kind
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for invalid kind")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test missing challenge tag
t.Run("Missing challenge tag", func(t *testing.T) {
ev := &event.T{
Pubkey: signer.Pub(),
CreatedAt: timestamp.Now(),
Kind: kind.ClientAuthentication,
Tags: tags.New(tag.New("relay", relayURL)),
}
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for missing challenge tag")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test missing relay tag
t.Run("Missing relay tag", func(t *testing.T) {
ev := &event.T{
Pubkey: signer.Pub(),
CreatedAt: timestamp.Now(),
Kind: kind.ClientAuthentication,
Tags: tags.New(tag.New("challenge", string(challenge))),
}
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for missing relay tag")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test URL scheme mismatch
t.Run("URL scheme mismatch", func(t *testing.T) {
ev := CreateUnsigned(signer.Pub(), challenge, "http://example.com")
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for URL scheme mismatch")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test URL host mismatch
t.Run("URL host mismatch", func(t *testing.T) {
ev := CreateUnsigned(signer.Pub(), challenge, "wss://other.com")
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for URL host mismatch")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test URL path mismatch
t.Run("URL path mismatch", func(t *testing.T) {
ev := CreateUnsigned(signer.Pub(), challenge, "wss://example.com/path")
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for URL path mismatch")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test timestamp out of range (future)
t.Run("Timestamp too far in future", func(t *testing.T) {
futureTime := timestamp.FromUnix(time.Now().Add(15 * time.Minute).Unix())
ev := &event.T{
Pubkey: signer.Pub(),
CreatedAt: futureTime,
Kind: kind.ClientAuthentication,
Tags: tags.New(tag.New("relay", relayURL), tag.New("challenge", string(challenge))),
}
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for timestamp too far in future")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test timestamp out of range (past)
t.Run("Timestamp too far in past", func(t *testing.T) {
pastTime := timestamp.FromUnix(time.Now().Add(-15 * time.Minute).Unix())
ev := &event.T{
Pubkey: signer.Pub(),
CreatedAt: pastTime,
Kind: kind.ClientAuthentication,
Tags: tags.New(tag.New("relay", relayURL), tag.New("challenge", string(challenge))),
}
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
ok, err := Validate(ev, challenge, relayURL)
if err == nil {
t.Error("Expected error for timestamp too far in past")
}
if ok {
t.Error("Expected validation to fail")
}
})
// Test invalid signature
t.Run("Invalid signature", func(t *testing.T) {
ev := createValidEvent()
// Corrupt the signature
ev.Sig[0] ^= 0xFF
ok, _ := Validate(ev, challenge, relayURL)
// It's acceptable for Validate to return either (false, nil) or (false, error)
// when the signature is invalid
if ok {
t.Error("Expected validation to fail for invalid signature")
}
})
// Test full happy path (already covered in TestCreateUnsigned, but adding for completeness)
t.Run("Full happy path", func(t *testing.T) {
const relayURL = "wss://example.com"
var ok bool
for range 10 {
challenge := GenerateChallenge()
ev := CreateUnsigned(signer.Pub(), challenge, relayURL)
if err = ev.Sign(signer); chk.E(err) {
t.Fatal(err)
}
if ok, err = Validate(ev, challenge, relayURL); chk.E(err) {
t.Fatal(err)
}
if !ok {
bb := ev.Marshal(nil)
t.Fatalf("failed to validate auth event\n%s", bb)
}
}
})
}