1
0

manager_reload_test.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package mtproto
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "testing"
  11. "time"
  12. )
  13. // TestMain lets the test binary re-exec itself as a stand-in for the mtg
  14. // child process: with MTG_FAKE_CHILD=1 it records its pid and blocks, so the
  15. // manager can start and stop it without a real mtg-multi binary.
  16. func TestMain(m *testing.M) {
  17. if os.Getenv("MTG_FAKE_CHILD") == "1" {
  18. if f, err := os.OpenFile(os.Getenv("MTG_FAKE_PIDFILE"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644); err == nil {
  19. fmt.Fprintf(f, "%d\n", os.Getpid())
  20. f.Close()
  21. }
  22. if exitFile := os.Getenv("MTG_FAKE_EXIT_FILE"); exitFile != "" {
  23. for {
  24. if _, err := os.Stat(exitFile); err == nil {
  25. os.Exit(1)
  26. } else if !os.IsNotExist(err) {
  27. os.Exit(2)
  28. }
  29. time.Sleep(time.Millisecond)
  30. }
  31. }
  32. select {}
  33. }
  34. os.Exit(m.Run())
  35. }
  36. func installFakeMtg(t *testing.T) string {
  37. t.Helper()
  38. binDir := t.TempDir()
  39. self, err := os.Executable()
  40. if err != nil {
  41. t.Fatalf("locate test binary: %v", err)
  42. }
  43. payload, err := os.ReadFile(self)
  44. if err != nil {
  45. t.Fatalf("read test binary: %v", err)
  46. }
  47. if err := os.WriteFile(filepath.Join(binDir, GetBinaryName()), payload, 0o755); err != nil {
  48. t.Fatalf("install fake mtg: %v", err)
  49. }
  50. pidFile := filepath.Join(binDir, "mtg-pids.txt")
  51. t.Setenv("XUI_BIN_FOLDER", binDir)
  52. t.Setenv("MTG_FAKE_CHILD", "1")
  53. t.Setenv("MTG_FAKE_PIDFILE", pidFile)
  54. return pidFile
  55. }
  56. func spawnCount(t *testing.T, pidFile string) int {
  57. t.Helper()
  58. data, err := os.ReadFile(pidFile)
  59. if os.IsNotExist(err) {
  60. return 0
  61. }
  62. if err != nil {
  63. t.Fatalf("read pid file: %v", err)
  64. }
  65. return len(strings.Fields(string(data)))
  66. }
  67. func waitSpawnCount(t *testing.T, pidFile string, want int) {
  68. t.Helper()
  69. deadline := time.Now().Add(5 * time.Second)
  70. for {
  71. got := spawnCount(t, pidFile)
  72. if got == want {
  73. return
  74. }
  75. if got > want {
  76. t.Fatalf("expected %d spawn(s), got %d", want, got)
  77. }
  78. if time.Now().After(deadline) {
  79. t.Fatalf("expected %d spawn(s), still %d after timeout", want, got)
  80. }
  81. time.Sleep(20 * time.Millisecond)
  82. }
  83. }
  84. func mtgInst(id int, secrets ...SecretEntry) Instance {
  85. return Instance{Id: id, Tag: fmt.Sprintf("inbound-%d", id), Listen: "127.0.0.1", Port: 24000 + id, Secrets: secrets}
  86. }
  87. func TestEnsureActionFor(t *testing.T) {
  88. cases := []struct {
  89. name string
  90. running bool
  91. curStruct, curSecrets, newStruct, newSecrets string
  92. want ensureAction
  93. }{
  94. {"dead process restarts", false, "s", "a", "s", "a", ensureRestart},
  95. {"structural change restarts", true, "s1", "a", "s2", "a", ensureRestart},
  96. {"secrets change reloads", true, "s", "a", "s", "b", ensureReload},
  97. {"identical is a noop", true, "s", "a", "s", "a", ensureNoop},
  98. {"dead beats a secrets-only change", false, "s", "a", "s", "b", ensureRestart},
  99. }
  100. for _, tc := range cases {
  101. t.Run(tc.name, func(t *testing.T) {
  102. if got := ensureActionFor(tc.running, tc.curStruct, tc.curSecrets, tc.newStruct, tc.newSecrets); got != tc.want {
  103. t.Fatalf("ensureActionFor = %d, want %d", got, tc.want)
  104. }
  105. })
  106. }
  107. }
  108. func TestApplySecrets(t *testing.T) {
  109. cases := []struct {
  110. name string
  111. status int
  112. want bool
  113. }{
  114. {"ok", http.StatusOK, true},
  115. {"not found on old binary", http.StatusNotFound, false},
  116. {"bad request", http.StatusBadRequest, false},
  117. {"unavailable", http.StatusServiceUnavailable, false},
  118. }
  119. for _, tc := range cases {
  120. t.Run(tc.name, func(t *testing.T) {
  121. var gotMethod, gotPath, gotAuth string
  122. var gotBody secretsPutBody
  123. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  124. gotMethod, gotPath, gotAuth = r.Method, r.URL.Path, r.Header.Get("Authorization")
  125. _ = json.NewDecoder(r.Body).Decode(&gotBody)
  126. w.WriteHeader(tc.status)
  127. }))
  128. defer srv.Close()
  129. inst := mtgInst(1,
  130. SecretEntry{Name: "alice", Secret: "ee01"},
  131. SecretEntry{Name: "bob", Secret: "ee02", AdTag: "fedcba9876543210fedcba9876543210"})
  132. if got := applySecrets(serverPort(t, srv), "sesame", inst); got != tc.want {
  133. t.Fatalf("applySecrets = %v, want %v", got, tc.want)
  134. }
  135. if gotMethod != http.MethodPut || gotPath != "/secrets" {
  136. t.Fatalf("expected PUT /secrets, got %s %s", gotMethod, gotPath)
  137. }
  138. if gotAuth != "Bearer sesame" {
  139. t.Fatalf("expected the bearer token on the request, got %q", gotAuth)
  140. }
  141. if gotBody.Secrets["alice"].Secret != "ee01" {
  142. t.Fatalf("payload must carry the secret: %+v", gotBody)
  143. }
  144. if gotBody.Secrets["alice"].AdTag != "" || gotBody.Secrets["bob"].AdTag != "fedcba9876543210fedcba9876543210" {
  145. t.Fatalf("payload must carry per-client ad-tags only where set: %+v", gotBody)
  146. }
  147. })
  148. }
  149. t.Run("refused connection", func(t *testing.T) {
  150. srv := httptest.NewServer(http.NotFoundHandler())
  151. port := serverPort(t, srv)
  152. srv.Close()
  153. if applySecrets(port, "", mtgInst(1, SecretEntry{Name: "a", Secret: "ee"})) {
  154. t.Fatal("a refused connection must yield false")
  155. }
  156. })
  157. }
  158. func TestEnsureHotReloadKeepsProcess(t *testing.T) {
  159. pidFile := installFakeMtg(t)
  160. mgr := &Manager{procs: map[int]*managed{}, swept: true}
  161. inst := mtgInst(1, SecretEntry{Name: "alice", Secret: "ee01"})
  162. if err := mgr.Ensure(inst); err != nil {
  163. t.Fatalf("initial ensure: %v", err)
  164. }
  165. waitSpawnCount(t, pidFile, 1)
  166. orig := mgr.procs[1].proc
  167. origToken := mgr.procs[1].apiToken
  168. if origToken == "" {
  169. t.Fatal("a started process must get an api token")
  170. }
  171. reloaded := make(chan struct{}, 1)
  172. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  173. if r.Method == http.MethodPut && r.URL.Path == "/secrets" {
  174. reloaded <- struct{}{}
  175. w.WriteHeader(http.StatusOK)
  176. return
  177. }
  178. http.NotFound(w, r)
  179. }))
  180. defer srv.Close()
  181. mgr.procs[1].apiPort = serverPort(t, srv)
  182. rekeyed := mtgInst(1, SecretEntry{Name: "alice", Secret: "ee01"}, SecretEntry{Name: "bob", Secret: "ee02"})
  183. if err := mgr.Ensure(rekeyed); err != nil {
  184. t.Fatalf("reload ensure: %v", err)
  185. }
  186. select {
  187. case <-reloaded:
  188. case <-time.After(3 * time.Second):
  189. t.Fatal("expected a PUT /secrets request")
  190. }
  191. if got := spawnCount(t, pidFile); got != 1 {
  192. t.Fatalf("hot reload must not spawn a new process, got %d", got)
  193. }
  194. if mgr.procs[1].proc != orig {
  195. t.Fatal("hot reload must keep the same process")
  196. }
  197. if mgr.procs[1].secretsFP != rekeyed.secretsFingerprint() {
  198. t.Fatal("stored secrets fingerprint must advance after a reload")
  199. }
  200. cfg, err := os.ReadFile(configPathForID(1))
  201. if err != nil {
  202. t.Fatalf("read config: %v", err)
  203. }
  204. if !strings.Contains(string(cfg), `"bob" = "ee02"`) {
  205. t.Fatalf("reloaded config must carry the new secret:\n%s", cfg)
  206. }
  207. if !strings.Contains(string(cfg), fmt.Sprintf("api-bind-to = \"127.0.0.1:%d\"", serverPort(t, srv))) {
  208. t.Fatalf("reload must reuse the same api port:\n%s", cfg)
  209. }
  210. if !strings.Contains(string(cfg), fmt.Sprintf("api-token = %q", origToken)) {
  211. t.Fatalf("reload must reuse the token the running process was started with:\n%s", cfg)
  212. }
  213. mgr.StopAll()
  214. }
  215. func TestEnsureReloadFallbackRestarts(t *testing.T) {
  216. pidFile := installFakeMtg(t)
  217. mgr := &Manager{procs: map[int]*managed{}, swept: true}
  218. if err := mgr.Ensure(mtgInst(2, SecretEntry{Name: "alice", Secret: "ee01"})); err != nil {
  219. t.Fatalf("initial ensure: %v", err)
  220. }
  221. waitSpawnCount(t, pidFile, 1)
  222. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  223. w.WriteHeader(http.StatusNotFound)
  224. }))
  225. defer srv.Close()
  226. mgr.procs[2].apiPort = serverPort(t, srv)
  227. if err := mgr.Ensure(mtgInst(2, SecretEntry{Name: "carol", Secret: "ee03"})); err != nil {
  228. t.Fatalf("fallback ensure: %v", err)
  229. }
  230. waitSpawnCount(t, pidFile, 2)
  231. mgr.StopAll()
  232. }
  233. func TestEnsureNoopKeepsProcess(t *testing.T) {
  234. pidFile := installFakeMtg(t)
  235. mgr := &Manager{procs: map[int]*managed{}, swept: true}
  236. inst := mtgInst(3, SecretEntry{Name: "alice", Secret: "ee01"}, SecretEntry{Name: "bob", Secret: "ee02"})
  237. if err := mgr.Ensure(inst); err != nil {
  238. t.Fatalf("initial ensure: %v", err)
  239. }
  240. waitSpawnCount(t, pidFile, 1)
  241. if err := mgr.Ensure(inst); err != nil {
  242. t.Fatalf("repeat ensure: %v", err)
  243. }
  244. reordered := mtgInst(3, SecretEntry{Name: "bob", Secret: "ee02"}, SecretEntry{Name: "alice", Secret: "ee01"})
  245. if err := mgr.Ensure(reordered); err != nil {
  246. t.Fatalf("reordered ensure: %v", err)
  247. }
  248. time.Sleep(300 * time.Millisecond)
  249. if got := spawnCount(t, pidFile); got != 1 {
  250. t.Fatalf("an unchanged instance must keep the one process, got %d spawns", got)
  251. }
  252. mgr.StopAll()
  253. }