storage_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "sync"
  8. "testing"
  9. "time"
  10. )
  11. // Tests share application globals and must not run in parallel.
  12. func resetLinks(t *testing.T) {
  13. t.Helper()
  14. clear := func() {
  15. links.Range(func(key, value interface{}) bool {
  16. links.Delete(key)
  17. return true
  18. })
  19. }
  20. clear()
  21. t.Cleanup(clear)
  22. }
  23. func TestAddLink(t *testing.T) {
  24. resetLinks(t)
  25. queue := make(chan string, 2)
  26. link := "https://example.com/path?q=one&other=two"
  27. ok, id := addLink(link, queue)
  28. if !ok {
  29. t.Fatalf("addLink failed: %s", id)
  30. }
  31. if got := getLink(id); got != link {
  32. t.Fatalf("stored URL = %q, want %q", got, link)
  33. }
  34. select {
  35. case got := <-queue:
  36. if want := fmt.Sprintf(format, id, link); got != want {
  37. t.Fatalf("queued record = %q, want %q", got, want)
  38. }
  39. default:
  40. t.Fatal("new link was not queued for saving")
  41. }
  42. if ok, got := addLink(link, queue); !ok || got != id {
  43. t.Fatalf("duplicate URL returned (%v, %q), want (true, %q)", ok, got, id)
  44. }
  45. if len(queue) != 0 {
  46. t.Fatal("duplicate URL was queued again")
  47. }
  48. }
  49. func TestAddLinkRejectsInvalidURL(t *testing.T) {
  50. for _, link := range []string{"http://%zz", "http://[::1", "https://example.com/\n"} {
  51. t.Run(link, func(t *testing.T) {
  52. resetLinks(t)
  53. queue := make(chan string, 1)
  54. ok, message := addLink(link, queue)
  55. if ok || !strings.Contains(message, "Error parsing link:") {
  56. t.Fatalf("invalid URL returned (%v, %q)", ok, message)
  57. }
  58. if len(queue) != 0 || getLink(getHash(link)) != "" {
  59. t.Fatal("invalid URL was stored or queued")
  60. }
  61. })
  62. }
  63. }
  64. func TestAddLinkRejectsCollision(t *testing.T) {
  65. resetLinks(t)
  66. first := "https://example.com/69897/4885590609"
  67. second := "https://example.com/105844/11202952336"
  68. queue := make(chan string, 2)
  69. ok, id := addLink(first, queue)
  70. if !ok {
  71. t.Fatalf("first URL rejected: %s", id)
  72. }
  73. if ok, message := addLink(second, queue); ok || message == "" {
  74. t.Fatalf("collision returned (%v, %q), want rejection", ok, message)
  75. }
  76. if getLink(id) != first || len(queue) != 1 {
  77. t.Fatal("collision replaced the original URL or queued another record")
  78. }
  79. }
  80. func TestAddLinkConcurrentDuplicates(t *testing.T) {
  81. resetLinks(t)
  82. const workers = 32
  83. queue := make(chan string, workers)
  84. link := "https://example.com/concurrent"
  85. var wg sync.WaitGroup
  86. for i := 0; i < workers; i++ {
  87. wg.Add(1)
  88. go func() {
  89. defer wg.Done()
  90. if ok, id := addLink(link, queue); !ok || id != getHash(link) {
  91. t.Errorf("addLink returned (%v, %q)", ok, id)
  92. }
  93. }()
  94. }
  95. wg.Wait()
  96. if len(queue) != 1 || getLink(getHash(link)) != link {
  97. t.Fatal("concurrent duplicates should store and queue exactly one mapping")
  98. }
  99. }
  100. func TestGetLinkMissing(t *testing.T) {
  101. resetLinks(t)
  102. if got := getLink("missing"); got != "" {
  103. t.Fatalf("missing URL = %q, want empty string", got)
  104. }
  105. }
  106. func TestReadLinks(t *testing.T) {
  107. resetLinks(t)
  108. path := filepath.Join(t.TempDir(), "links.txt")
  109. content := "one<>https://example.com/first\ntwo<>https://example.com/?q=<>\n"
  110. if err := os.WriteFile(path, []byte(content), 0600); err != nil {
  111. t.Fatal(err)
  112. }
  113. readLinks(path)
  114. for id, want := range map[string]string{
  115. "one": "https://example.com/first",
  116. "two": "https://example.com/?q=<>",
  117. } {
  118. if got := getLink(id); got != want {
  119. t.Fatalf("getLink(%q) = %q, want %q", id, got, want)
  120. }
  121. }
  122. }
  123. func TestReadLinksCreatesMissingFile(t *testing.T) {
  124. resetLinks(t)
  125. path := filepath.Join(t.TempDir(), "links.txt")
  126. readLinks(path)
  127. info, err := os.Stat(path)
  128. if err != nil {
  129. t.Fatal(err)
  130. }
  131. if info.Size() != 0 {
  132. t.Fatal("new storage file should be empty")
  133. }
  134. }
  135. func TestSaveLinkAppendsAndReloads(t *testing.T) {
  136. resetLinks(t)
  137. path := filepath.Join(t.TempDir(), "links.txt")
  138. initial := "old<>https://example.com/old\n"
  139. if err := os.WriteFile(path, []byte(initial), 0600); err != nil {
  140. t.Fatal(err)
  141. }
  142. queue := make(chan string, 2)
  143. queue <- "new<>https://example.com/new\n"
  144. queue <- "query<>https://example.com/?q=<>\n"
  145. close(queue)
  146. done := make(chan struct{})
  147. go func() {
  148. saveLink(path, queue)
  149. close(done)
  150. }()
  151. select {
  152. case <-done:
  153. case <-time.After(5 * time.Second):
  154. t.Fatal("saveLink did not stop after the queue closed")
  155. }
  156. content, err := os.ReadFile(path)
  157. if err != nil {
  158. t.Fatal(err)
  159. }
  160. want := initial + "new<>https://example.com/new\nquery<>https://example.com/?q=<>\n"
  161. if string(content) != want {
  162. t.Fatalf("saved content = %q, want %q", content, want)
  163. }
  164. readLinks(path)
  165. for id, want := range map[string]string{
  166. "old": "https://example.com/old",
  167. "new": "https://example.com/new",
  168. "query": "https://example.com/?q=<>",
  169. } {
  170. if got := getLink(id); got != want {
  171. t.Fatalf("reloaded %q = %q, want %q", id, got, want)
  172. }
  173. }
  174. }