326 lines
8.8 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|