kerberos_test.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package kerberos
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "log/slog"
  8. "net"
  9. "net/http"
  10. "sync"
  11. "testing"
  12. "time"
  13. "github.com/mk6i/open-oscar-server/config"
  14. "github.com/mk6i/open-oscar-server/wire"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/mock"
  17. )
  18. func TestKerberosLoginHandler(t *testing.T) {
  19. tests := []struct {
  20. name string
  21. listeners []config.Listener
  22. request wire.SNACMessage
  23. response wire.SNACMessage
  24. responseErr error
  25. expectLogin bool
  26. expectSNACResponse bool
  27. wantStatus int
  28. }{
  29. {
  30. name: "successful login with single listener",
  31. listeners: []config.Listener{
  32. {
  33. KerberosListenAddress: ":1088",
  34. BOSAdvertisedHostPlain: "localhost:5190",
  35. },
  36. },
  37. request: wire.SNACMessage{
  38. Frame: wire.SNACFrame{
  39. FoodGroup: wire.Kerberos,
  40. SubGroup: wire.KerberosLoginRequest,
  41. },
  42. Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
  43. RequestID: 4321,
  44. },
  45. },
  46. response: wire.SNACMessage{
  47. Frame: wire.SNACFrame{
  48. FoodGroup: wire.Kerberos,
  49. SubGroup: wire.KerberosLoginSuccessResponse,
  50. },
  51. Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
  52. RequestID: 4321,
  53. },
  54. },
  55. expectLogin: true,
  56. expectSNACResponse: true,
  57. wantStatus: http.StatusOK,
  58. },
  59. {
  60. name: "successful login with multiple listeners",
  61. listeners: []config.Listener{
  62. {
  63. KerberosListenAddress: ":1088",
  64. BOSAdvertisedHostPlain: "localhost:5190",
  65. },
  66. {
  67. KerberosListenAddress: ":1089",
  68. BOSAdvertisedHostPlain: "localhost:5191",
  69. },
  70. },
  71. request: wire.SNACMessage{
  72. Frame: wire.SNACFrame{
  73. FoodGroup: wire.Kerberos,
  74. SubGroup: wire.KerberosLoginRequest,
  75. },
  76. Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
  77. RequestID: 4321,
  78. },
  79. },
  80. response: wire.SNACMessage{
  81. Frame: wire.SNACFrame{
  82. FoodGroup: wire.Kerberos,
  83. SubGroup: wire.KerberosLoginSuccessResponse,
  84. },
  85. Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
  86. RequestID: 4321,
  87. },
  88. },
  89. expectLogin: true,
  90. expectSNACResponse: true,
  91. wantStatus: http.StatusOK,
  92. },
  93. {
  94. name: "successful login with three listeners",
  95. listeners: []config.Listener{
  96. {
  97. KerberosListenAddress: ":1088",
  98. BOSAdvertisedHostPlain: "localhost:5190",
  99. },
  100. {
  101. KerberosListenAddress: ":1089",
  102. BOSAdvertisedHostPlain: "localhost:5191",
  103. },
  104. {
  105. KerberosListenAddress: ":1090",
  106. BOSAdvertisedHostPlain: "localhost:5192",
  107. },
  108. },
  109. request: wire.SNACMessage{
  110. Frame: wire.SNACFrame{
  111. FoodGroup: wire.Kerberos,
  112. SubGroup: wire.KerberosLoginRequest,
  113. },
  114. Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
  115. RequestID: 4321,
  116. },
  117. },
  118. response: wire.SNACMessage{
  119. Frame: wire.SNACFrame{
  120. FoodGroup: wire.Kerberos,
  121. SubGroup: wire.KerberosLoginSuccessResponse,
  122. },
  123. Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
  124. RequestID: 4321,
  125. },
  126. },
  127. expectLogin: true,
  128. expectSNACResponse: true,
  129. wantStatus: http.StatusOK,
  130. },
  131. {
  132. name: "no kerberos listeners defined - server exits cleanly",
  133. listeners: []config.Listener{
  134. {
  135. BOSAdvertisedHostPlain: "localhost:5192",
  136. },
  137. },
  138. request: wire.SNACMessage{},
  139. response: wire.SNACMessage{},
  140. responseErr: nil,
  141. expectLogin: false,
  142. expectSNACResponse: false,
  143. wantStatus: 0, // No server to test against
  144. },
  145. {
  146. name: "invalid request SNAC type",
  147. listeners: []config.Listener{
  148. {
  149. KerberosListenAddress: ":1088",
  150. BOSAdvertisedHostPlain: "localhost:5190",
  151. },
  152. },
  153. request: wire.SNACMessage{
  154. Frame: wire.SNACFrame{
  155. FoodGroup: wire.ICBM,
  156. SubGroup: wire.ICBMChannelMsgToHost,
  157. },
  158. Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
  159. RequestID: 4321,
  160. },
  161. },
  162. expectLogin: false,
  163. expectSNACResponse: false,
  164. wantStatus: http.StatusBadRequest,
  165. },
  166. {
  167. name: "login runtime error",
  168. listeners: []config.Listener{
  169. {
  170. KerberosListenAddress: ":1088",
  171. BOSAdvertisedHostPlain: "localhost:5190",
  172. },
  173. },
  174. request: wire.SNACMessage{
  175. Frame: wire.SNACFrame{
  176. FoodGroup: wire.Kerberos,
  177. SubGroup: wire.KerberosLoginRequest,
  178. },
  179. Body: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{
  180. RequestID: 4321,
  181. },
  182. },
  183. response: wire.SNACMessage{},
  184. responseErr: io.EOF,
  185. expectLogin: true,
  186. expectSNACResponse: false,
  187. wantStatus: http.StatusInternalServerError,
  188. },
  189. }
  190. for _, tt := range tests {
  191. t.Run(tt.name, func(t *testing.T) {
  192. log := slog.New(slog.NewTextHandler(io.Discard, nil))
  193. var srv *Server
  194. if len(tt.listeners) > 0 {
  195. mockAuth := newMockAuthService(t)
  196. if tt.expectLogin {
  197. mockAuth.EXPECT().
  198. KerberosLogin(mock.Anything, tt.request.Body, mock.Anything).
  199. Return(tt.response, tt.responseErr)
  200. }
  201. srv = NewKerberosServer(tt.listeners, log, mockAuth)
  202. } else {
  203. // For no listeners case, we don't need auth service or request data
  204. srv = NewKerberosServer(tt.listeners, log, nil)
  205. }
  206. wg := sync.WaitGroup{}
  207. wg.Add(1)
  208. go func() {
  209. defer wg.Done()
  210. assert.NoError(t, srv.ListenAndServe())
  211. }()
  212. // Wait for server to be ready by checking if ports are listening
  213. for i := 0; i < len(tt.listeners); i++ {
  214. if tt.listeners[i].KerberosListenAddress == "" {
  215. continue
  216. }
  217. maxRetries := 10
  218. backoff := 5 * time.Millisecond
  219. for attempt := 0; attempt < maxRetries; attempt++ {
  220. conn, err := net.Dial("tcp", "localhost"+tt.listeners[i].KerberosListenAddress)
  221. if err == nil {
  222. _ = conn.Close()
  223. break
  224. }
  225. if attempt == maxRetries-1 {
  226. t.Fatalf("Server not ready after %d attempts: %v", maxRetries, err)
  227. }
  228. time.Sleep(backoff)
  229. backoff *= 2
  230. }
  231. }
  232. // Test against all listeners
  233. for i, listener := range tt.listeners {
  234. if listener.KerberosListenAddress == "" {
  235. continue
  236. }
  237. b := &bytes.Buffer{}
  238. assert.NoError(t, wire.MarshalBE(tt.request, b))
  239. resp, err := http.Post(fmt.Sprintf("http://localhost:%s", listener.KerberosListenAddress[1:]), "application/x-snac", b)
  240. assert.NoError(t, err)
  241. defer func() { _ = resp.Body.Close() }()
  242. assert.Equal(t, tt.wantStatus, resp.StatusCode, "listener %d at %s", i, listener.KerberosListenAddress)
  243. if tt.expectSNACResponse {
  244. respBytes, _ := io.ReadAll(resp.Body)
  245. reader := bytes.NewReader(respBytes)
  246. haveFrame := wire.SNACFrame{}
  247. assert.NoError(t, wire.UnmarshalBE(&haveFrame, reader))
  248. assert.Equal(t, tt.response.Frame, haveFrame)
  249. haveBody := wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{}
  250. assert.NoError(t, wire.UnmarshalBE(&haveBody, reader))
  251. assert.Equal(t, tt.response.Body, haveBody)
  252. assert.Equal(t, "application/x-snac", resp.Header.Get("Content-Type"))
  253. } else {
  254. assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
  255. }
  256. }
  257. assert.NoError(t, srv.Shutdown(context.Background()))
  258. wg.Wait()
  259. })
  260. }
  261. }