main_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package main
  2. import (
  3. "bytes"
  4. "net/http"
  5. "net/http/httptest"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. "time"
  11. )
  12. // newTestServer returns an httptest server that maps API keys to status codes.
  13. // Keys ending with "-401" return 401; "-422" return 422; everything else returns 200 with the sample body.
  14. func newTestServer(t *testing.T) *httptest.Server {
  15. t.Helper()
  16. sample, err := os.ReadFile(filepath.Join("..", "..", "internal", "api", "testdata", "sample.json"))
  17. if err != nil {
  18. t.Fatalf("read sample: %v", err)
  19. }
  20. return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  21. auth := r.Header.Get("Authorization")
  22. switch {
  23. case strings.HasSuffix(auth, "-401"):
  24. w.WriteHeader(http.StatusUnauthorized)
  25. case strings.HasSuffix(auth, "-422"):
  26. w.WriteHeader(http.StatusUnprocessableEntity)
  27. default:
  28. w.Header().Set("Content-Type", "application/json")
  29. _, _ = w.Write(sample)
  30. }
  31. }))
  32. }
  33. func writeConfig(t *testing.T, body string) string {
  34. t.Helper()
  35. dir := t.TempDir()
  36. p := filepath.Join(dir, "config.yaml")
  37. if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
  38. t.Fatal(err)
  39. }
  40. return p
  41. }
  42. func TestRun_Version(t *testing.T) {
  43. var out, errb bytes.Buffer
  44. code := run([]string{"--version"}, &out, &errb)
  45. if code != exitOK {
  46. t.Errorf("exit = %d want %d", code, exitOK)
  47. }
  48. if !strings.Contains(out.String(), "zenmux-usage") {
  49. t.Errorf("version output missing program name: %q", out.String())
  50. }
  51. }
  52. func TestRun_UnknownAccountExits2(t *testing.T) {
  53. cfg := writeConfig(t, `
  54. accounts:
  55. - name: personal
  56. api_key: sk-p
  57. `)
  58. var out, errb bytes.Buffer
  59. code := run([]string{"--config", cfg, "--account", "missing"}, &out, &errb)
  60. if code != exitUsage {
  61. t.Errorf("exit = %d want %d, stderr=%q", code, exitUsage, errb.String())
  62. }
  63. if !strings.Contains(errb.String(), "account not found") {
  64. t.Errorf("stderr missing 'account not found': %q", errb.String())
  65. }
  66. }
  67. func TestRun_NoConfigNoEnvExits3(t *testing.T) {
  68. t.Setenv("ZENMUX_MANAGEMENT_API_KEY", "")
  69. t.Setenv("XDG_CONFIG_HOME", t.TempDir()) // point default path at an empty dir
  70. var out, errb bytes.Buffer
  71. code := run(nil, &out, &errb)
  72. if code != exitNoAccount {
  73. t.Errorf("exit = %d want %d, stderr=%q", code, exitNoAccount, errb.String())
  74. }
  75. }
  76. func TestRun_ExplicitConfigMissingExits3(t *testing.T) {
  77. var out, errb bytes.Buffer
  78. code := run([]string{"--config", "/nonexistent/does/not/exist.yaml"}, &out, &errb)
  79. if code != exitNoAccount {
  80. t.Errorf("exit = %d want %d, stderr=%q", code, exitNoAccount, errb.String())
  81. }
  82. }
  83. func TestRun_MalformedConfigExits7(t *testing.T) {
  84. cfg := writeConfig(t, "accounts:\n - name: a\n api_key: [")
  85. var out, errb bytes.Buffer
  86. code := run([]string{"--config", cfg}, &out, &errb)
  87. if code != exitConfigParse {
  88. t.Errorf("exit = %d want %d, stderr=%q", code, exitConfigParse, errb.String())
  89. }
  90. }
  91. func TestRun_InvalidTimeoutExits2(t *testing.T) {
  92. var out, errb bytes.Buffer
  93. code := run([]string{"--timeout", "0s"}, &out, &errb)
  94. if code != exitUsage {
  95. t.Errorf("exit = %d want %d", code, exitUsage)
  96. }
  97. }
  98. func TestRun_UnexpectedPositionalExits2(t *testing.T) {
  99. var out, errb bytes.Buffer
  100. code := run([]string{"extra"}, &out, &errb)
  101. if code != exitUsage {
  102. t.Errorf("exit = %d want %d", code, exitUsage)
  103. }
  104. }
  105. func TestRun_HelpExits0(t *testing.T) {
  106. var out, errb bytes.Buffer
  107. code := run([]string{"--help"}, &out, &errb)
  108. if code != exitOK {
  109. t.Errorf("exit = %d want %d", code, exitOK)
  110. }
  111. }
  112. func TestRun_SingleAccountSuccess(t *testing.T) {
  113. srv := newTestServer(t)
  114. defer srv.Close()
  115. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  116. var out, errb bytes.Buffer
  117. code := run([]string{"--api-key", "sk-ok", "--no-color"}, &out, &errb)
  118. if code != exitOK {
  119. t.Fatalf("exit = %d want %d; stderr=%q", code, exitOK, errb.String())
  120. }
  121. if !strings.Contains(out.String(), "Ultra plan") {
  122. t.Errorf("expected Ultra plan in output, got %q", out.String())
  123. }
  124. }
  125. func TestRun_SingleAccountAuthFailExits4(t *testing.T) {
  126. srv := newTestServer(t)
  127. defer srv.Close()
  128. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  129. var out, errb bytes.Buffer
  130. code := run([]string{"--api-key", "sk-401", "--no-color"}, &out, &errb)
  131. if code != exitAuth {
  132. t.Fatalf("exit = %d want %d", code, exitAuth)
  133. }
  134. }
  135. func TestRun_MultiAccountMixedExits1(t *testing.T) {
  136. srv := newTestServer(t)
  137. defer srv.Close()
  138. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  139. cfg := writeConfig(t, `
  140. accounts:
  141. - name: ok
  142. api_key: sk-ok
  143. - name: bad
  144. api_key: sk-401
  145. `)
  146. var out, errb bytes.Buffer
  147. code := run([]string{"--config", cfg, "--no-color"}, &out, &errb)
  148. if code != exitGeneric {
  149. t.Fatalf("exit = %d want %d (mixed)", code, exitGeneric)
  150. }
  151. if !strings.Contains(out.String(), "━━━ ok ━━━") || !strings.Contains(out.String(), "━━━ bad ━━━") {
  152. t.Errorf("both account headers should appear; got %q", out.String())
  153. }
  154. if !strings.Contains(out.String(), "error:") {
  155. t.Errorf("failed account should have error line; got %q", out.String())
  156. }
  157. }
  158. func TestRun_AllRateLimitedExits5(t *testing.T) {
  159. srv := newTestServer(t)
  160. defer srv.Close()
  161. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  162. cfg := writeConfig(t, `
  163. accounts:
  164. - name: a
  165. api_key: sk-422
  166. - name: b
  167. api_key: sk-422
  168. `)
  169. var out, errb bytes.Buffer
  170. code := run([]string{"--config", cfg, "--no-color"}, &out, &errb)
  171. if code != exitRateLimited {
  172. t.Fatalf("exit = %d want %d", code, exitRateLimited)
  173. }
  174. }
  175. func TestRun_JSONSingleAccountPassthrough(t *testing.T) {
  176. srv := newTestServer(t)
  177. defer srv.Close()
  178. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  179. var out, errb bytes.Buffer
  180. code := run([]string{"--api-key", "sk-ok", "--json"}, &out, &errb)
  181. if code != exitOK {
  182. t.Fatalf("exit = %d want %d", code, exitOK)
  183. }
  184. if !strings.Contains(out.String(), `"tier": "ultra"`) {
  185. t.Errorf("expected raw passthrough including plan tier; got %q", out.String())
  186. }
  187. }
  188. func TestRun_AllNetworkTimeoutExits6(t *testing.T) {
  189. // Slow server forces a timeout.
  190. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  191. time.Sleep(200 * time.Millisecond)
  192. }))
  193. defer srv.Close()
  194. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  195. var out, errb bytes.Buffer
  196. code := run([]string{"--api-key", "sk-slow", "--timeout", "10ms", "--no-color"}, &out, &errb)
  197. if code != exitNetwork {
  198. t.Fatalf("exit = %d want %d", code, exitNetwork)
  199. }
  200. }
  201. func TestRun_JSONMultiAccount(t *testing.T) {
  202. srv := newTestServer(t)
  203. defer srv.Close()
  204. t.Setenv("ZENMUX_BASE_URL", srv.URL)
  205. cfg := writeConfig(t, `
  206. accounts:
  207. - name: ok
  208. api_key: sk-ok
  209. - name: bad
  210. api_key: sk-401
  211. `)
  212. var out, errb bytes.Buffer
  213. code := run([]string{"--config", cfg, "--json"}, &out, &errb)
  214. // Mixed outcomes → 1
  215. if code != exitGeneric {
  216. t.Fatalf("exit = %d want %d", code, exitGeneric)
  217. }
  218. // Array shape.
  219. o := strings.TrimSpace(out.String())
  220. if !strings.HasPrefix(o, "[") || !strings.HasSuffix(o, "]") {
  221. t.Errorf("expected JSON array, got %q", o)
  222. }
  223. if !strings.Contains(o, `"account": "ok"`) || !strings.Contains(o, `"account": "bad"`) {
  224. t.Errorf("missing account keys in multi JSON: %q", o)
  225. }
  226. }