proxy_binding_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package server
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "log/slog"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "vocat/internal/store"
  11. )
  12. func TestDeviceProxyBindingPersistsAndReconnectsEnabledVoWiFi(t *testing.T) {
  13. database, err := store.Open(context.Background(), ":memory:")
  14. if err != nil {
  15. t.Fatal(err)
  16. }
  17. t.Cleanup(func() { _ = database.Close() })
  18. if err := database.UpsertDevice(context.Background(), store.Device{
  19. ID: "ec20", Name: "EC20", VoWiFiEnabled: true,
  20. }); err != nil {
  21. t.Fatal(err)
  22. }
  23. if err := database.UpsertUpstreamProxy(context.Background(), store.UpstreamProxy{
  24. ID: "route-1", Name: "Route 1", Addr: "127.0.0.1:1080", Enabled: true,
  25. }); err != nil {
  26. t.Fatal(err)
  27. }
  28. controller := &fakeVoWiFiController{}
  29. server := &Server{
  30. store: database,
  31. vowifi: controller,
  32. logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
  33. maxRequestBodyBytes: 4096,
  34. }
  35. request := httptest.NewRequest(
  36. http.MethodPut,
  37. "/api/upstream-proxy-device-bindings/ec20",
  38. bytes.NewBufferString(`{"upstream_proxy_id":"route-1"}`),
  39. )
  40. request.Header.Set("Content-Type", "application/json")
  41. response := httptest.NewRecorder()
  42. server.handleDeviceProxyBinding(response, request, "ec20")
  43. if response.Code != http.StatusOK {
  44. t.Fatalf("PUT status = %d, body = %s", response.Code, response.Body.String())
  45. }
  46. binding, err := database.DeviceProxyBinding(context.Background(), "ec20")
  47. if err != nil || binding.UpstreamProxyID != "route-1" {
  48. t.Fatalf("binding = %+v, %v", binding, err)
  49. }
  50. if controller.reconnects != 1 {
  51. t.Fatalf("reconnects = %d, want 1", controller.reconnects)
  52. }
  53. request = httptest.NewRequest(http.MethodDelete, "/api/upstream-proxy-device-bindings/ec20", nil)
  54. response = httptest.NewRecorder()
  55. server.handleDeviceProxyBinding(response, request, "ec20")
  56. if response.Code != http.StatusOK {
  57. t.Fatalf("DELETE status = %d, body = %s", response.Code, response.Body.String())
  58. }
  59. if _, err := database.DeviceProxyBinding(context.Background(), "ec20"); err != store.ErrNotFound {
  60. t.Fatalf("binding after delete error = %v, want ErrNotFound", err)
  61. }
  62. if controller.reconnects != 2 {
  63. t.Fatalf("reconnects = %d, want 2", controller.reconnects)
  64. }
  65. }
  66. func TestDeviceProxyBindingRejectsRebindToDifferentUpstream(t *testing.T) {
  67. database, err := store.Open(context.Background(), ":memory:")
  68. if err != nil {
  69. t.Fatal(err)
  70. }
  71. t.Cleanup(func() { _ = database.Close() })
  72. if err := database.UpsertDevice(context.Background(), store.Device{
  73. ID: "ec20", Name: "EC20", VoWiFiEnabled: true,
  74. }); err != nil {
  75. t.Fatal(err)
  76. }
  77. for _, up := range []store.UpstreamProxy{
  78. {ID: "route-1", Name: "Route 1", Addr: "127.0.0.1:1080", Enabled: true},
  79. {ID: "route-2", Name: "Route 2", Addr: "127.0.0.1:1081", Enabled: true},
  80. } {
  81. if err := database.UpsertUpstreamProxy(context.Background(), up); err != nil {
  82. t.Fatal(err)
  83. }
  84. }
  85. server := &Server{
  86. store: database,
  87. vowifi: &fakeVoWiFiController{},
  88. logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
  89. maxRequestBodyBytes: 4096,
  90. }
  91. // First bind to route-1 succeeds.
  92. put := func(proxyID string) *httptest.ResponseRecorder {
  93. req := httptest.NewRequest(
  94. http.MethodPut,
  95. "/api/upstream-proxy-device-bindings/ec20",
  96. bytes.NewBufferString(`{"upstream_proxy_id":"`+proxyID+`"}`),
  97. )
  98. req.Header.Set("Content-Type", "application/json")
  99. rec := httptest.NewRecorder()
  100. server.handleDeviceProxyBinding(rec, req, "ec20")
  101. return rec
  102. }
  103. if rec := put("route-1"); rec.Code != http.StatusOK {
  104. t.Fatalf("initial bind status = %d, body = %s", rec.Code, rec.Body.String())
  105. }
  106. // Rebind to a different upstream must be rejected with 409.
  107. rec := put("route-2")
  108. if rec.Code != http.StatusConflict {
  109. t.Fatalf("rebind status = %d, want 409, body = %s", rec.Code, rec.Body.String())
  110. }
  111. binding, err := database.DeviceProxyBinding(context.Background(), "ec20")
  112. if err != nil || binding.UpstreamProxyID != "route-1" {
  113. t.Fatalf("binding after rejected rebind = %+v, %v (want route-1 unchanged)", binding, err)
  114. }
  115. // Re-binding the SAME upstream stays idempotent (no 409).
  116. if rec := put("route-1"); rec.Code != http.StatusOK {
  117. t.Fatalf("idempotent rebind status = %d, want 200, body = %s", rec.Code, rec.Body.String())
  118. }
  119. }