2026-02-18 23:23:34 +08:00

244 lines
6.5 KiB
Go

package main
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
"github.com/heroiclabs/nakama-common/rtapi"
"github.com/heroiclabs/nakama-go"
)
const (
ServerKey = "defaultkey"
Host = "127.0.0.1"
Port = 7350 // HTTP port
Scheme = "http" // or https
)
func main() {
// Seed random number generator
rand.Seed(time.Now().UnixNano())
fmt.Println("=== Starting Matchmaker Tests ===")
// Test 1: Free vs Free (Should Match)
fmt.Println("\n[Test 1] Free vs Free (Expect Success)")
if err := runMatchTest("minesweeper_free", "minesweeper_free", true); err != nil {
log.Printf("Test 1 Failed: %v", err)
} else {
fmt.Println("✅ Test 1 Passed")
}
// Test 2: Paid vs Paid (Should Match)
fmt.Println("\n[Test 2] Paid vs Paid (Expect Success)")
if err := runMatchTest("minesweeper", "minesweeper", true); err != nil {
log.Printf("Test 2 Failed: %v", err)
} else {
fmt.Println("✅ Test 2 Passed")
}
// Test 3: Mixed (Free vs Paid) - Should NOT match if queries correct
// Note: Nakama Matchmaker simply matches based on query.
// If User A queries "type:free" and User B queries "type:paid", they WON'T match anyway.
// But if we force a match using wide query "*" but different props, Server Hook should REJECT.
fmt.Println("\n[Test 3] Mixed Properties (Wide Query) (Expect Rejection by Hook)")
if err := runMixedTest(); err != nil {
// If error returned (e.g. timeout), it means no match formed = Success (Hook rejected or Matchmaker ignored)
fmt.Println("✅ Test 3 Passed (No Match formed/accepted)")
} else {
log.Printf("❌ Test 3 Failed: Mixed match was created!")
}
// Test 4: Missing Property (Should Fail)
fmt.Println("\n[Test 4] Missing Property (Expect Rejection)")
if err := runMissingPropertyTest(); err != nil {
fmt.Println("✅ Test 4 Passed (Match rejected/failed as expected)")
} else {
log.Printf("❌ Test 4 Failed: Match created without game_type property!")
}
}
func runMatchTest(type1, type2 string, expectSuccess bool) error {
var wg sync.WaitGroup
wg.Add(2)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
errChan := make(chan error, 2)
matchChan := make(chan string, 2)
go runClient(ctx, &wg, type1, "*", errChan, matchChan) // Use wildcard query to test Hook validation? No, behave normally first.
// Actually, accurate tests should use accurate queries.
// But to test the HOOK, we want them to MATCH in matchmaker but fail in HOOK.
// Nakama Matchmaker is very efficient. If queries don't overlap, they won't match.
// To test "3 Free 1 Paid matched successfully" implies their queries OVERLAPPED.
// So we use Query="*" for all tests to simulate "bad queries" and rely on HOOK validation.
go runClient(ctx, &wg, type2, "*", errChan, matchChan)
// Wait for completion
go func() {
wg.Wait()
close(matchChan)
close(errChan)
}()
// We need 2 matches
matches := 0
for {
select {
case err, ok := <-errChan:
if ok {
return err
}
case _, ok := <-matchChan:
if !ok {
// closed
if matches == 2 {
return nil
}
if expectSuccess {
return fmt.Errorf("timeout/insufficient matches")
}
return fmt.Errorf("expected failure") // Treated as success for negative test
}
matches++
if matches == 2 {
return nil
}
case <-ctx.Done():
if expectSuccess {
return fmt.Errorf("timeout")
}
return fmt.Errorf("timeout (expected)")
}
}
}
func runMixedTest() error {
var wg sync.WaitGroup
wg.Add(2)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
errChan := make(chan error, 2)
matchChan := make(chan string, 2)
// One Free, One Paid. Both use "*" query to force Nakama to try matching them.
// Server Hook SHOULD check props and reject.
go runClient(ctx, &wg, "minesweeper_free", "*", errChan, matchChan)
go runClient(ctx, &wg, "minesweeper", "*", errChan, matchChan)
// Same wait logic...
// We expect timeout (no match ID returned) or error.
matches := 0
for {
select {
case <-ctx.Done():
return fmt.Errorf("timeout") // Good result for this test
case _, ok := <-matchChan:
if ok {
matches++
if matches == 2 {
return nil // Bad result! Match succeeded
}
}
}
}
}
func runMissingPropertyTest() error {
var wg sync.WaitGroup
wg.Add(2)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
matchChan := make(chan string, 2)
runBadClient := func() {
defer wg.Done()
client := nakama.NewClient(ServerKey, Host, Port, Scheme)
id := fmt.Sprintf("bad_%d", rand.Int())
session, err := client.AuthenticateCustom(ctx, id, true, "")
if err != nil {
return
}
socket := client.NewSocket()
socket.Connect(ctx, session, true)
msgChan := make(chan *rtapi.MatchmakerMatched, 1)
socket.SetMatchmakerMatchedFn(func(m *rtapi.MatchmakerMatched) {
msgChan <- m
})
// Add matchmaker with NO properties, strict fallback check in server should activate
socket.AddMatchmaker(ctx, "*", 2, 2, nil, nil)
select {
case m := <-msgChan:
matchChan <- m.MatchId
case <-ctx.Done():
}
}
go runBadClient()
go runBadClient()
wg.Wait()
if len(matchChan) > 0 {
return nil // Bad
}
return fmt.Errorf("no match") // Good
}
func runClient(ctx context.Context, wg *sync.WaitGroup, gameType string, query string, errChan chan error, matchChan chan string) {
defer wg.Done()
client := nakama.NewClient(ServerKey, Host, Port, Scheme)
id := fmt.Sprintf("u_%s_%d", gameType, rand.Int())
session, err := client.AuthenticateCustom(ctx, id, true, "")
if err != nil {
errChan <- err
return
}
socket := client.NewSocket()
if err := socket.Connect(ctx, session, true); err != nil {
errChan <- err
return
}
props := map[string]string{"game_type": gameType}
// Use query if provided, else construct one
q := query
if q == "" {
q = fmt.Sprintf("+properties.game_type:%s", gameType)
}
log.Printf("[%s] Adding to matchmaker (Query: %s)", id, q)
msgChan := make(chan *rtapi.MatchmakerMatched, 1)
socket.SetMatchmakerMatchedFn(func(m *rtapi.MatchmakerMatched) {
msgChan <- m
})
_, err = socket.AddMatchmaker(ctx, q, 2, 2, props, nil)
if err != nil {
errChan <- err
return
}
select {
case m := <-msgChan:
log.Printf("[%s] MATCHED! MatchID: %s", id, m.MatchId)
matchChan <- m.MatchId
// Join attempt?
// Logic: If Hook succeeds, MatchmakerMatched is sent.
// If Hook fails (returns error), MatchmakerMatched is NOT sent (Nakama aborts match).
// So receiving this message implies Hook accepted it.
case <-ctx.Done():
log.Printf("[%s] Timeout", id)
}
}