Files
rudder-sdk-go/client_test.go
T
2026-11-01 13:04:02 +03:00

255 lines
7.0 KiB
Go

package server
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
type request struct {
method string
path string
headers http.Header
body string
}
func testClient(t *testing.T, handler http.Handler) *Client {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return New(Options{
ServerKey: "server_key",
Address: server.URL + "/",
})
}
func readBody(t *testing.T, r *http.Request) string {
t.Helper()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
return string(body)
}
func TestQuerySendsThePlayerAndDecodesTheResult(t *testing.T) {
var sent request
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sent = request{method: r.Method, path: r.URL.Path, headers: r.Header, body: readBody(t, r)}
_, _ = w.Write([]byte(`{"result": {"asset": "gems", "amount": 5}}`))
}))
amount, err := Query[Amount](context.Background(), client, "wallet", "balance", "player-1", nil)
if err != nil {
t.Fatal(err)
}
if amount.Asset != "gems" || amount.Amount != 5 {
t.Fatalf("amount %+v, want 5 gems", amount)
}
if sent.method != http.MethodPost || sent.path != "/server/v1/players/player-1/modules/wallet/balance" {
t.Fatalf("request %s %s, want POST the module of the player", sent.method, sent.path)
}
if sent.headers.Get("X-Rudder-Server-Key") != "server_key" {
t.Fatalf("server key header %q", sent.headers.Get("X-Rudder-Server-Key"))
}
if sent.headers.Get("Idempotency-Key") != "" {
t.Fatalf("a query sent the idempotency key %q", sent.headers.Get("Idempotency-Key"))
}
if sent.body != `{"args":{}}` {
t.Fatalf("query body %s", sent.body)
}
}
func TestMutationGeneratesAndReusesTheCallerKey(t *testing.T) {
var sent request
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sent = request{method: r.Method, path: r.URL.Path, headers: r.Header, body: readBody(t, r)}
_, _ = w.Write([]byte(`{"result": {"offer": "welcome"}}`))
}))
input := struct {
Offer string `json:"offer"`
}{Offer: "welcome"}
result, err := Mutation[struct {
Offer string `json:"offer"`
}](context.Background(), client, "stores", "buy", "player-1", input, nil)
if err != nil {
t.Fatal(err)
}
if result.Offer != "welcome" {
t.Fatalf("result %+v, want the welcome offer", result)
}
if key := sent.headers.Get("Idempotency-Key"); len(key) != 32 {
t.Fatalf("generated key %q, want 32 hex characters", key)
}
if sent.body != `{"args":{"offer":"welcome"}}` {
t.Fatalf("mutation body %s", sent.body)
}
client = testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sent = request{headers: r.Header, body: readBody(t, r)}
_, _ = w.Write([]byte(`{"result": null}`))
}))
if _, err := Mutation[struct{}](context.Background(), client, "stores", "reset", "player-1", nil, &MutationOptions{IdempotencyKey: "mine"}); err != nil {
t.Fatal(err)
}
if sent.headers.Get("Idempotency-Key") != "mine" {
t.Fatalf("key %q, want the caller's", sent.headers.Get("Idempotency-Key"))
}
}
func TestEnvironmentReadsTheKeyEnvironment(t *testing.T) {
var sent request
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sent = request{method: r.Method, path: r.URL.Path, headers: r.Header, body: readBody(t, r)}
_, _ = w.Write([]byte(`{"environment": "staging"}`))
}))
environment, err := client.Environment(context.Background())
if err != nil {
t.Fatal(err)
}
if environment != Staging {
t.Fatalf("environment %q, want staging", environment)
}
if sent.method != http.MethodGet || sent.path != "/server/v1/environment" {
t.Fatalf("request %s %s", sent.method, sent.path)
}
if sent.body != "" {
t.Fatalf("environment body %q, want none", sent.body)
}
}
func TestProfileBanAndUnban(t *testing.T) {
var sent []request
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sent = append(sent, request{method: r.Method, path: r.URL.Path, headers: r.Header, body: readBody(t, r)})
w.WriteHeader(http.StatusNoContent)
}))
until := time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)
if err := client.SetPlayerProfile(context.Background(), "player-1", Profile{Nickname: "Ada", Avatar: "https://example.com/ada.png"}); err != nil {
t.Fatal(err)
}
if err := client.BanPlayer(context.Background(), "player-1", Ban{Until: until, Reason: "cheating"}); err != nil {
t.Fatal(err)
}
if err := client.UnbanPlayer(context.Background(), "player-1"); err != nil {
t.Fatal(err)
}
if len(sent) != 3 {
t.Fatalf("%d requests, want three", len(sent))
}
if sent[0].method != http.MethodPut || sent[0].path != "/server/v1/players/player-1/profile" ||
sent[0].body != `{"nickname":"Ada","avatar":"https://example.com/ada.png"}` {
t.Fatalf("profile request %+v", sent[0])
}
if sent[1].method != http.MethodPut || sent[1].path != "/server/v1/players/player-1/ban" ||
sent[1].body != `{"until":"2026-12-01T00:00:00Z","reason":"cheating"}` {
t.Fatalf("ban request %+v", sent[1])
}
if sent[2].method != http.MethodDelete || sent[2].path != "/server/v1/players/player-1/ban" || sent[2].body != "" {
t.Fatalf("unban request %+v", sent[2])
}
}
func TestTryAgainKeepsTheSameKey(t *testing.T) {
var attempts int32
var keys []string
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&attempts, 1)
keys = append(keys, r.Header.Get("Idempotency-Key"))
if len(keys) == 1 {
w.Header().Set("Retry-After", "0")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write([]byte(`{"error": {"code": "try_again", "message": "try again"}}`))
return
}
_, _ = w.Write([]byte(`{"result": null}`))
}))
if _, err := Mutation[struct{}](context.Background(), client, "stores", "reset", "player-1", nil, nil); err != nil {
t.Fatal(err)
}
if attempts != 2 {
t.Fatalf("%d attempts, want two", attempts)
}
if keys[0] != keys[1] {
t.Fatalf("keys %v, want the same", keys)
}
}
func TestRefusalIsAnErrorWithTheCodeAndStatus(t *testing.T) {
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error": {"code": "player_not_found", "message": "no such player"}}`))
}))
_, err := Query[Amount](context.Background(), client, "wallet", "balance", "player-1", nil)
if err == nil {
t.Fatal("a refusal: no error")
}
var failure *Error
if !errors.As(err, &failure) {
t.Fatalf("error %T, want *server.Error", err)
}
if failure.Code != "player_not_found" || failure.Status != http.StatusNotFound {
t.Fatalf("error %+v", failure)
}
}
func TestAnswerThatIsNotJSON(t *testing.T) {
client := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("not json"))
}))
_, err := Query[Amount](context.Background(), client, "wallet", "balance", "player-1", nil)
var failure *Error
if !errors.As(err, &failure) || failure.Code != "invalid_response" {
t.Fatalf("error %v, want invalid_response", err)
}
}