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