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) } } }) }