252 lines
6.1 KiB
Go
Executable File

package main
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
"github.com/ascii8/nakama-go"
)
const (
ServerKey = "defaultkey"
URL = "http://127.0.0.1:7350" // Assuming default local Nakama
)
func main() {
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: Free vs Paid (Should NOT Match)
fmt.Println("\n[Test 3] Mixed Properties (Wide Query) (Expect Rejection by Hook)")
if err := runMixedTest(); err != nil {
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 getClient() *nakama.Client {
return nakama.New(
nakama.WithServerKey(ServerKey),
nakama.WithURL(URL),
)
}
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)
// Use wildcard query "*" to force potential match, relying on PROPERTY validation by server hook
// OR use specific query +properties.game_type:X to be realistic.
// Since we want to test if "3 free 1 paid" match, let's use specific queries first to ensure basic flows work.
// Actually, the user's issue was "3 free 1 paid matched".
// If they used specific queries, they wouldn't match at all in Matchmaker.
// The fact they matched implies they might be using broad queries or the client logic has a fallback query.
// But let's assume specific queries for "Success" tests and "*" for "Mixed/Failure" tests.
q1 := fmt.Sprintf("+properties.game_type:%s", type1)
q2 := fmt.Sprintf("+properties.game_type:%s", type2)
go runClient(ctx, &wg, type1, q1, errChan, matchChan)
go runClient(ctx, &wg, type2, q2, errChan, matchChan)
go func() {
wg.Wait()
close(matchChan)
close(errChan)
}()
matches := 0
for {
select {
case err, ok := <-errChan:
if ok {
return err
}
case _, ok := <-matchChan:
if !ok {
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)
// Force them to 'see' each other with wildcard query
go runClient(ctx, &wg, "minesweeper_free", "*", errChan, matchChan)
go runClient(ctx, &wg, "minesweeper", "*", errChan, matchChan)
matches := 0
for {
select {
case <-ctx.Done():
return fmt.Errorf("timeout") // Good result
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 := getClient()
id := fmt.Sprintf("bad_%d", rand.Int())
// Authenticate
err := client.AuthenticateCustom(ctx, id, true, "")
if err != nil {
return
}
// NewConn
conn, err := client.NewConn(ctx)
if err != nil {
return
}
conn.MatchmakerMatchedHandler = func(ctx context.Context, m *nakama.MatchmakerMatchedMsg) {
matchChan <- m.GetMatchId()
}
if err := conn.Open(ctx); err != nil {
return
}
// Add matchmaker with NO properties
msg := nakama.MatchmakerAdd("*", 2, 2)
conn.MatchmakerAdd(ctx, msg)
select {
case <-ctx.Done():
}
}
go runBadClient()
go runBadClient()
// Wait a bit
time.Sleep(2 * time.Second)
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 := getClient()
uID := fmt.Sprintf("u_%s_%d", gameType, rand.Int())
if err := client.AuthenticateCustom(ctx, uID, true, ""); err != nil {
errChan <- fmt.Errorf("auth failed: %w", err)
return
}
conn, err := client.NewConn(ctx)
if err != nil {
errChan <- fmt.Errorf("newconn failed: %w", err)
return
}
conn.MatchmakerMatchedHandler = func(ctx context.Context, m *nakama.MatchmakerMatchedMsg) {
id := m.GetMatchId()
token := m.GetToken()
log.Printf("[%s] MATCHED! ID: %q, Token: %q", uID, id, token)
if id == "" && token == "" {
return // Ignore empty match? verification needed
}
if id != "" {
matchChan <- id
} else {
matchChan <- token
}
}
if err := conn.Open(ctx); err != nil {
errChan <- fmt.Errorf("conn open failed: %w", err)
return
}
props := map[string]string{"game_type": gameType}
msg := nakama.MatchmakerAdd(query, 2, 2).WithStringProperties(props)
log.Printf("[%s] Adding to matchmaker (Query: %s, Props: %v)", uID, query, props)
if _, err := conn.MatchmakerAdd(ctx, msg); err != nil {
errChan <- fmt.Errorf("add matchmaker failed: %w", err)
return
}
select {
case <-ctx.Done():
}
}