client_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package gog
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "os"
  7. "strings"
  8. "testing"
  9. )
  10. // serveHome serves the given HTML for all requests.
  11. func serveHome(t *testing.T, html string) *Client {
  12. t.Helper()
  13. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  14. w.Header().Set("Content-Type", "text/html; charset=UTF-8")
  15. w.Write([]byte(html))
  16. }))
  17. t.Cleanup(srv.Close)
  18. c := NewClient()
  19. c.HTTP = srv.Client()
  20. // Point the client at the test server by overriding the home URL via
  21. // BaseURL-like behavior: homeURL is a package const, so we test through
  22. // the extraction path instead.
  23. _ = srv
  24. return c
  25. }
  26. // The live homepage embeds the giveaway in gogcom-store-state JSON.
  27. func TestLiveFixture_GiveawayFromStoreState(t *testing.T) {
  28. html, err := os.ReadFile("testdata/home.html")
  29. if err != nil {
  30. t.Fatal(err)
  31. }
  32. state := extractStoreState(string(html))
  33. if state == "" {
  34. t.Fatal("gogcom-store-state script not found in live fixture")
  35. }
  36. sec, err := parseGiveawaySection([]byte(state))
  37. if err != nil {
  38. t.Fatalf("parse giveaway: %v", err)
  39. }
  40. if sec == nil {
  41. t.Fatal("no giveaway section found in live fixture")
  42. }
  43. if sec.Product == nil || sec.Product.Title == "" || sec.Product.Slug == "" {
  44. t.Fatalf("giveaway product incomplete: %+v", sec.Product)
  45. }
  46. if sec.Product.Slug != "a_new_beginning" {
  47. t.Errorf("slug = %q, want a_new_beginning (live giveaway Oct 2026)", sec.Product.Slug)
  48. }
  49. if sec.Product.ID == "" {
  50. t.Error("product id missing")
  51. }
  52. if sec.EndDate.IsZero() {
  53. t.Error("giveaway endDate missing")
  54. }
  55. }
  56. // Regression: the section is identified by STRUCTURE (endDate + product with
  57. // slug/title), not by a fixed section UUID — survives layout changes.
  58. func TestGiveawayRecognitionIsStructural(t *testing.T) {
  59. // A section with endDate + product anywhere in the state is found,
  60. // even under an arbitrary key.
  61. state := `{"someKey":{"x":{"endDate":"2026-11-02T16:00:59.000+02:00","product":{"id":"123","title":"Test Game","slug":"test_game"}}},"other":{"endDate":"2020-01-01T00:00:00Z","product":{"id":"0","title":"Old","slug":"old"}}}`
  62. sec, err := parseGiveawaySection([]byte(state))
  63. if err != nil {
  64. t.Fatal(err)
  65. }
  66. if sec == nil || sec.Product.Slug != "test_game" {
  67. t.Fatalf("structural detection failed: %+v", sec)
  68. }
  69. }
  70. // No store-state script → explicit error (the client must not pretend
  71. // success with an empty list).
  72. func TestMissingStoreStateIsAnError(t *testing.T) {
  73. if state := extractStoreState("<html><body>nothing here</body></html>"); state != "" {
  74. t.Fatalf("expected empty state, got %q", state)
  75. }
  76. }
  77. // A store-state without any giveaway object → nil section, no error.
  78. func TestStateWithoutGiveawayYieldsNoSection(t *testing.T) {
  79. sec, err := parseGiveawaySection([]byte(`{"pageMetadata":{"locale":"en-US"}}`))
  80. if err != nil {
  81. t.Fatal(err)
  82. }
  83. if sec != nil {
  84. t.Fatalf("expected no section, got %+v", sec)
  85. }
  86. }
  87. // Broken store-state JSON → error, not panic.
  88. func TestBrokenStoreStateJSONIsAnError(t *testing.T) {
  89. if _, err := parseGiveawaySection([]byte("{not json")); err == nil {
  90. t.Fatal("expected error for broken JSON")
  91. }
  92. }
  93. // The extraction handles the script tag with trailing content after </script>.
  94. func TestExtractStoreStateTrimsScript(t *testing.T) {
  95. html := `<html><head><script id="gogcom-store-state" type="application/json">{"a":1}</script></head></html>`
  96. state := extractStoreState(html)
  97. if strings.TrimSpace(state) != `{"a":1}` {
  98. t.Fatalf("state = %q", state)
  99. }
  100. }
  101. // End-to-end via HTTP: client fetches the (test) homepage and extracts the
  102. // giveaway. Uses a package-level URL override to avoid network.
  103. func TestGetFreeGamesEndToEnd(t *testing.T) {
  104. html, err := os.ReadFile("testdata/home.html")
  105. if err != nil {
  106. t.Fatal(err)
  107. }
  108. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  109. w.Write(html)
  110. }))
  111. t.Cleanup(srv.Close)
  112. c := NewClient()
  113. c.HTTP = srv.Client()
  114. c.HomeURL = srv.URL
  115. games, err := c.GetFreeGames(context.Background())
  116. if err != nil {
  117. t.Fatal(err)
  118. }
  119. if len(games) != 1 {
  120. t.Fatalf("want exactly 1 giveaway, got %d", len(games))
  121. }
  122. g := games[0]
  123. if g.Store != "gog" || g.Kind != "free-now" {
  124. t.Errorf("store/kind = %s/%s, want gog/free-now", g.Store, g.Kind)
  125. }
  126. if !strings.HasPrefix(g.ID, "gog-") {
  127. t.Errorf("id = %q, want gog- prefix", g.ID)
  128. }
  129. if g.URL == "" {
  130. t.Error("url missing")
  131. }
  132. }