- Introduced a new skill for Golang, providing comprehensive guidance on writing, debugging, and best practices for Go programming. - Added reference materials including effective Go guidelines, common patterns, and a quick reference cheat sheet to support users in Go development. - Created a skill creator guide to assist in developing new skills with structured templates and resource management. - Implemented scripts for skill initialization and packaging to streamline the skill creation process.
452 lines
12 KiB
Go
452 lines
12 KiB
Go
package relaytester
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"lol.mleku.dev/errorf"
|
|
)
|
|
|
|
// TestResult represents the result of a test.
|
|
type TestResult struct {
|
|
Name string `json:"test"`
|
|
Pass bool `json:"pass"`
|
|
Required bool `json:"required"`
|
|
Info string `json:"info,omitempty"`
|
|
}
|
|
|
|
// TestFunc is a function that runs a test case.
|
|
type TestFunc func(client *Client, key1, key2 *KeyPair) (result TestResult)
|
|
|
|
// TestCase represents a test case with dependencies.
|
|
type TestCase struct {
|
|
Name string
|
|
Required bool
|
|
Func TestFunc
|
|
Dependencies []string // Names of tests that must run before this one
|
|
}
|
|
|
|
// TestSuite runs all tests against a relay.
|
|
type TestSuite struct {
|
|
relayURL string
|
|
key1 *KeyPair
|
|
key2 *KeyPair
|
|
tests map[string]*TestCase
|
|
results map[string]TestResult
|
|
order []string
|
|
}
|
|
|
|
// NewTestSuite creates a new test suite.
|
|
func NewTestSuite(relayURL string) (suite *TestSuite, err error) {
|
|
suite = &TestSuite{
|
|
relayURL: relayURL,
|
|
tests: make(map[string]*TestCase),
|
|
results: make(map[string]TestResult),
|
|
}
|
|
if suite.key1, err = GenerateKeyPair(); err != nil {
|
|
return
|
|
}
|
|
if suite.key2, err = GenerateKeyPair(); err != nil {
|
|
return
|
|
}
|
|
suite.registerTests()
|
|
return
|
|
}
|
|
|
|
// AddTest adds a test case to the suite.
|
|
func (s *TestSuite) AddTest(tc *TestCase) {
|
|
s.tests[tc.Name] = tc
|
|
}
|
|
|
|
// registerTests registers all test cases.
|
|
func (s *TestSuite) registerTests() {
|
|
allTests := []*TestCase{
|
|
{
|
|
Name: "Publishes basic event",
|
|
Required: true,
|
|
Func: testPublishBasicEvent,
|
|
},
|
|
{
|
|
Name: "Finds event by ID",
|
|
Required: true,
|
|
Func: testFindByID,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds event by author",
|
|
Required: true,
|
|
Func: testFindByAuthor,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds event by kind",
|
|
Required: true,
|
|
Func: testFindByKind,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds event by tags",
|
|
Required: true,
|
|
Func: testFindByTags,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by multiple tags",
|
|
Required: true,
|
|
Func: testFindByMultipleTags,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by time range",
|
|
Required: true,
|
|
Func: testFindByTimeRange,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Rejects invalid signature",
|
|
Required: true,
|
|
Func: testRejectInvalidSignature,
|
|
},
|
|
{
|
|
Name: "Rejects future event",
|
|
Required: true,
|
|
Func: testRejectFutureEvent,
|
|
},
|
|
{
|
|
Name: "Rejects expired event",
|
|
Required: false,
|
|
Func: testRejectExpiredEvent,
|
|
},
|
|
{
|
|
Name: "Handles replaceable events",
|
|
Required: true,
|
|
Func: testReplaceableEvents,
|
|
},
|
|
{
|
|
Name: "Handles ephemeral events",
|
|
Required: false,
|
|
Func: testEphemeralEvents,
|
|
},
|
|
{
|
|
Name: "Handles parameterized replaceable events",
|
|
Required: true,
|
|
Func: testParameterizedReplaceableEvents,
|
|
},
|
|
{
|
|
Name: "Handles deletion events",
|
|
Required: true,
|
|
Func: testDeletionEvents,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Handles COUNT request",
|
|
Required: true,
|
|
Func: testCountRequest,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Handles limit parameter",
|
|
Required: true,
|
|
Func: testLimitParameter,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Handles multiple filters",
|
|
Required: true,
|
|
Func: testMultipleFilters,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Handles subscription close",
|
|
Required: true,
|
|
Func: testSubscriptionClose,
|
|
},
|
|
// Filter tests
|
|
{
|
|
Name: "Since and until filters are inclusive",
|
|
Required: true,
|
|
Func: testSinceUntilAreInclusive,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Limit zero works",
|
|
Required: true,
|
|
Func: testLimitZero,
|
|
},
|
|
// Find tests
|
|
{
|
|
Name: "Events are ordered from newest to oldest",
|
|
Required: true,
|
|
Func: testEventsOrderedFromNewestToOldest,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Newest events are returned when filter is limited",
|
|
Required: true,
|
|
Func: testNewestEventsWhenLimited,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by pubkey and kind",
|
|
Required: true,
|
|
Func: testFindByPubkeyAndKind,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by pubkey and tags",
|
|
Required: true,
|
|
Func: testFindByPubkeyAndTags,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by kind and tags",
|
|
Required: true,
|
|
Func: testFindByKindAndTags,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Finds by scrape",
|
|
Required: true,
|
|
Func: testFindByScrape,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
// Replaceable event tests
|
|
{
|
|
Name: "Replaces metadata",
|
|
Required: true,
|
|
Func: testReplacesMetadata,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Replaces contact list",
|
|
Required: true,
|
|
Func: testReplacesContactList,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Replaced events are still available by ID",
|
|
Required: false,
|
|
Func: testReplacedEventsStillAvailableByID,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Replaceable events replace older ones",
|
|
Required: true,
|
|
Func: testReplaceableEventRemovesPrevious,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Replaceable events rejected if a newer one exists",
|
|
Required: true,
|
|
Func: testReplaceableEventRejectedIfFuture,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Addressable events replace older ones",
|
|
Required: true,
|
|
Func: testAddressableEventRemovesPrevious,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Addressable events rejected if a newer one exists",
|
|
Required: true,
|
|
Func: testAddressableEventRejectedIfFuture,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
// Deletion tests
|
|
{
|
|
Name: "Deletes by a-tag address",
|
|
Required: true,
|
|
Func: testDeleteByAddr,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Delete by a-tag deletes older but not newer",
|
|
Required: true,
|
|
Func: testDeleteByAddrOnlyDeletesOlder,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Delete by a-tag is bound by a-tag",
|
|
Required: true,
|
|
Func: testDeleteByAddrIsBoundByTag,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
// Ephemeral tests
|
|
{
|
|
Name: "Ephemeral subscriptions work",
|
|
Required: false,
|
|
Func: testEphemeralSubscriptionsWork,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Persists ephemeral events",
|
|
Required: false,
|
|
Func: testPersistsEphemeralEvents,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
// EOSE tests
|
|
{
|
|
Name: "Supports EOSE",
|
|
Required: true,
|
|
Func: testSupportsEose,
|
|
},
|
|
{
|
|
Name: "Subscription receives event after ping period",
|
|
Required: true,
|
|
Func: testSubscriptionReceivesEventAfterPingPeriod,
|
|
},
|
|
{
|
|
Name: "Closes complete subscriptions after EOSE",
|
|
Required: false,
|
|
Func: testClosesCompleteSubscriptionsAfterEose,
|
|
},
|
|
{
|
|
Name: "Keeps open incomplete subscriptions after EOSE",
|
|
Required: true,
|
|
Func: testKeepsOpenIncompleteSubscriptionsAfterEose,
|
|
},
|
|
// JSON tests
|
|
{
|
|
Name: "Accepts events with empty tags",
|
|
Required: false,
|
|
Func: testAcceptsEventsWithEmptyTags,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
{
|
|
Name: "Accepts NIP-01 JSON escape sequences",
|
|
Required: true,
|
|
Func: testAcceptsNip1JsonEscapeSequences,
|
|
Dependencies: []string{"Publishes basic event"},
|
|
},
|
|
// Registration tests
|
|
{
|
|
Name: "Sends OK after EVENT",
|
|
Required: true,
|
|
Func: testSendsOkAfterEvent,
|
|
},
|
|
{
|
|
Name: "Verifies event signatures",
|
|
Required: true,
|
|
Func: testVerifiesSignatures,
|
|
},
|
|
{
|
|
Name: "Verifies event ID hashes",
|
|
Required: true,
|
|
Func: testVerifiesIdHashes,
|
|
},
|
|
}
|
|
for _, tc := range allTests {
|
|
s.AddTest(tc)
|
|
}
|
|
s.topologicalSort()
|
|
}
|
|
|
|
// topologicalSort orders tests based on dependencies.
|
|
func (s *TestSuite) topologicalSort() {
|
|
visited := make(map[string]bool)
|
|
temp := make(map[string]bool)
|
|
var visit func(name string)
|
|
visit = func(name string) {
|
|
if temp[name] {
|
|
return
|
|
}
|
|
if visited[name] {
|
|
return
|
|
}
|
|
temp[name] = true
|
|
if tc, exists := s.tests[name]; exists {
|
|
for _, dep := range tc.Dependencies {
|
|
visit(dep)
|
|
}
|
|
}
|
|
temp[name] = false
|
|
visited[name] = true
|
|
s.order = append(s.order, name)
|
|
}
|
|
for name := range s.tests {
|
|
if !visited[name] {
|
|
visit(name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run runs all tests in the suite.
|
|
func (s *TestSuite) Run() (results []TestResult, err error) {
|
|
for _, name := range s.order {
|
|
tc := s.tests[name]
|
|
if tc == nil {
|
|
continue
|
|
}
|
|
// Create a new client for each test to avoid connection issues
|
|
client, clientErr := NewClient(s.relayURL)
|
|
if clientErr != nil {
|
|
return nil, errorf.E("failed to connect to relay: %w", clientErr)
|
|
}
|
|
result := tc.Func(client, s.key1, s.key2)
|
|
result.Name = name
|
|
result.Required = tc.Required
|
|
s.results[name] = result
|
|
results = append(results, result)
|
|
client.Close()
|
|
time.Sleep(100 * time.Millisecond) // Small delay between tests
|
|
}
|
|
return
|
|
}
|
|
|
|
// RunTest runs a specific test by name.
|
|
func (s *TestSuite) RunTest(testName string) (result TestResult, err error) {
|
|
tc, exists := s.tests[testName]
|
|
if !exists {
|
|
return result, errorf.E("test %s not found", testName)
|
|
}
|
|
// Check dependencies
|
|
for _, dep := range tc.Dependencies {
|
|
if _, exists := s.results[dep]; !exists {
|
|
return result, errorf.E("test %s depends on %s which has not been run", testName, dep)
|
|
}
|
|
if !s.results[dep].Pass {
|
|
return result, errorf.E("test %s depends on %s which failed", testName, dep)
|
|
}
|
|
}
|
|
// Create a new client for the test
|
|
client, clientErr := NewClient(s.relayURL)
|
|
if clientErr != nil {
|
|
return result, errorf.E("failed to connect to relay: %w", clientErr)
|
|
}
|
|
defer client.Close()
|
|
result = tc.Func(client, s.key1, s.key2)
|
|
result.Name = testName
|
|
result.Required = tc.Required
|
|
s.results[testName] = result
|
|
return
|
|
}
|
|
|
|
// GetResults returns all test results.
|
|
func (s *TestSuite) GetResults() map[string]TestResult {
|
|
return s.results
|
|
}
|
|
|
|
// ListTests returns a list of all test names in execution order.
|
|
func (s *TestSuite) ListTests() []string {
|
|
return s.order
|
|
}
|
|
|
|
// GetTestNames returns all registered test names as a map (name -> required).
|
|
func (s *TestSuite) GetTestNames() map[string]bool {
|
|
result := make(map[string]bool)
|
|
for name, tc := range s.tests {
|
|
result[name] = tc.Required
|
|
}
|
|
return result
|
|
}
|
|
|
|
// FormatJSON formats results as JSON.
|
|
func FormatJSON(results []TestResult) (output string, err error) {
|
|
var data []byte
|
|
if data, err = json.Marshal(results); err != nil {
|
|
return
|
|
}
|
|
return string(data), nil
|
|
}
|