auth_test.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package handlers
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. "github.com/google/uuid"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. // testAuthService implements AuthService for ClientLogin tests (only FLAPLogin is exercised).
  16. type testAuthService struct {
  17. flapLogin func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error)
  18. }
  19. func (t *testAuthService) BUCPChallenge(ctx context.Context, bodyIn wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error) {
  20. return wire.SNACMessage{}, nil
  21. }
  22. func (t *testAuthService) BUCPLogin(ctx context.Context, bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error) {
  23. return wire.SNACMessage{}, nil
  24. }
  25. func (t *testAuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error) {
  26. return state.ServerCookie{}, nil
  27. }
  28. func (t *testAuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error) {
  29. return nil, nil
  30. }
  31. func (t *testAuthService) FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  32. if t.flapLogin != nil {
  33. return t.flapLogin(ctx, inFrame, advertisedHost)
  34. }
  35. return wire.TLVRestBlock{}, nil
  36. }
  37. func (t *testAuthService) Signout(ctx context.Context, session *state.Session) {}
  38. func (t *testAuthService) SignoutChat(ctx context.Context, sess *state.Session) {}
  39. func successfulLoginBlock() wire.TLVRestBlock {
  40. var b wire.TLVRestBlock
  41. b.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("fake-auth-cookie-bytes")))
  42. return b
  43. }
  44. func failedLoginBlock() wire.TLVRestBlock {
  45. var b wire.TLVRestBlock
  46. b.Append(wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, uint16(1)))
  47. return b
  48. }
  49. type testCookieBaker struct {
  50. issue func(data []byte) ([]byte, error)
  51. }
  52. func (t *testCookieBaker) Issue(data []byte) ([]byte, error) {
  53. if t.issue != nil {
  54. return t.issue(data)
  55. }
  56. return []byte("issued-cookie"), nil
  57. }
  58. func (t *testCookieBaker) Crack(data []byte) ([]byte, error) {
  59. return data, nil
  60. }
  61. func TestAuthHandler_GetToken(t *testing.T) {
  62. tests := []struct {
  63. name string
  64. query string
  65. cookies []*http.Cookie
  66. checkBody func(*testing.T, string)
  67. expectedCode int
  68. }{
  69. {
  70. name: "Success_LocalAuthUserCookie",
  71. query: "f=json&attributes=loginId&devId=ao1yOLlHVHhsa3o6&c=_callbacks_._0mq8wqdav",
  72. cookies: []*http.Cookie{
  73. {Name: "localAuthUser", Value: "testuser||Test User"},
  74. },
  75. checkBody: func(t *testing.T, body string) {
  76. assert.Contains(t, body, "_callbacks_._0mq8wqdav(")
  77. assert.Contains(t, body, `"statusCode":200`)
  78. assert.Contains(t, body, `"loginId":"testuser"`)
  79. assert.Contains(t, body, `"a":`)
  80. },
  81. expectedCode: http.StatusOK,
  82. },
  83. {
  84. name: "Unauthorized_NoSession",
  85. query: "f=json&attributes=loginId&devId=ao1yOLlHVHhsa3o6&c=_callbacks_._abc",
  86. checkBody: func(t *testing.T, body string) {
  87. assert.Contains(t, body, `"statusCode":401`)
  88. assert.Contains(t, body, `"redirectURL"`)
  89. },
  90. expectedCode: http.StatusOK,
  91. },
  92. {
  93. // getToken no longer checks account existence; an unknown screen name
  94. // still receives a token. Existence is enforced later by
  95. // RegisterBOSSession during startSession.
  96. name: "Success_UnknownUserStillIssuesToken",
  97. query: "f=json&attributes=loginId&devId=dev123&c=_callbacks_._xyz",
  98. cookies: []*http.Cookie{
  99. {Name: "localAuthUser", Value: "missing||Missing User"},
  100. },
  101. checkBody: func(t *testing.T, body string) {
  102. assert.Contains(t, body, `"statusCode":200`)
  103. assert.Contains(t, body, `"loginId":"missing"`)
  104. assert.Contains(t, body, `"a":`)
  105. },
  106. expectedCode: http.StatusOK,
  107. },
  108. }
  109. for _, tt := range tests {
  110. t.Run(tt.name, func(t *testing.T) {
  111. handler := &AuthHandler{
  112. AuthService: &testAuthService{},
  113. CookieBaker: &testCookieBaker{},
  114. Logger: slog.Default(),
  115. }
  116. req, err := http.NewRequest(http.MethodGet, "/auth/getToken?"+tt.query, nil)
  117. assert.NoError(t, err)
  118. for _, c := range tt.cookies {
  119. req.AddCookie(c)
  120. }
  121. rr := httptest.NewRecorder()
  122. handler.GetToken(rr, req)
  123. assert.Equal(t, tt.expectedCode, rr.Code)
  124. if tt.checkBody != nil {
  125. tt.checkBody(t, rr.Body.String())
  126. }
  127. })
  128. }
  129. }
  130. func TestAuthHandler_ClientLogin(t *testing.T) {
  131. tests := []struct {
  132. name string
  133. method string
  134. contentType string
  135. body string
  136. auth *testAuthService
  137. expectedStatusCode int
  138. checkResponse func(*testing.T, string)
  139. }{
  140. {
  141. name: "Success_JSONBody",
  142. method: "POST",
  143. contentType: "application/json",
  144. body: `{"username":"testuser","password":"testpass","devId":"dev123"}`,
  145. auth: &testAuthService{
  146. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  147. return successfulLoginBlock(), nil
  148. },
  149. },
  150. expectedStatusCode: http.StatusOK,
  151. checkResponse: func(t *testing.T, body string) {
  152. assert.Contains(t, body, `"statusCode":200`)
  153. assert.Contains(t, body, `"loginId":"testuser"`)
  154. assert.Contains(t, body, `"screenName":"testuser"`)
  155. assert.Contains(t, body, `"token"`)
  156. assert.Contains(t, body, `"sessionSecret"`)
  157. },
  158. },
  159. {
  160. name: "Success_FormEncoded",
  161. method: "POST",
  162. contentType: "application/x-www-form-urlencoded",
  163. body: "s=testuser&pwd=testpass&devId=dev123",
  164. auth: &testAuthService{
  165. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  166. return successfulLoginBlock(), nil
  167. },
  168. },
  169. expectedStatusCode: http.StatusOK,
  170. checkResponse: func(t *testing.T, body string) {
  171. assert.Contains(t, body, `"statusCode":200`)
  172. assert.Contains(t, body, `"loginId":"testuser"`)
  173. },
  174. },
  175. {
  176. name: "Error_MissingUsername",
  177. method: "POST",
  178. contentType: "application/json",
  179. body: `{"username":"","password":"testpass"}`,
  180. auth: &testAuthService{},
  181. expectedStatusCode: http.StatusBadRequest,
  182. checkResponse: func(t *testing.T, body string) {
  183. assert.Contains(t, body, "username and password required")
  184. },
  185. },
  186. {
  187. name: "Error_MissingPassword",
  188. method: "POST",
  189. contentType: "application/json",
  190. body: `{"username":"testuser","password":""}`,
  191. auth: &testAuthService{},
  192. expectedStatusCode: http.StatusBadRequest,
  193. checkResponse: func(t *testing.T, body string) {
  194. assert.Contains(t, body, "username and password required")
  195. },
  196. },
  197. {
  198. name: "Error_AuthFailed",
  199. method: "POST",
  200. contentType: "application/json",
  201. body: `{"username":"testuser","password":"wrongpass"}`,
  202. auth: &testAuthService{
  203. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  204. return failedLoginBlock(), nil
  205. },
  206. },
  207. expectedStatusCode: http.StatusUnauthorized,
  208. checkResponse: func(t *testing.T, body string) {
  209. assert.Contains(t, body, "username and password required")
  210. },
  211. },
  212. {
  213. name: "Error_FLAPLoginError",
  214. method: "POST",
  215. contentType: "application/json",
  216. body: `{"username":"testuser","password":"testpass"}`,
  217. auth: &testAuthService{
  218. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  219. return wire.TLVRestBlock{}, errors.New("boom")
  220. },
  221. },
  222. expectedStatusCode: http.StatusInternalServerError,
  223. checkResponse: func(t *testing.T, body string) {
  224. assert.Contains(t, body, "internal server error")
  225. },
  226. },
  227. {
  228. name: "Error_InvalidJSON",
  229. method: "POST",
  230. contentType: "application/json",
  231. body: `{invalid json`,
  232. auth: &testAuthService{},
  233. expectedStatusCode: http.StatusBadRequest,
  234. checkResponse: func(t *testing.T, body string) {
  235. assert.Contains(t, body, "invalid JSON format")
  236. },
  237. },
  238. }
  239. for _, tt := range tests {
  240. t.Run(tt.name, func(t *testing.T) {
  241. logger := slog.Default()
  242. handler := &AuthHandler{
  243. AuthService: tt.auth,
  244. Logger: logger,
  245. }
  246. req, err := http.NewRequest(tt.method, "/auth/clientLogin", strings.NewReader(tt.body))
  247. assert.NoError(t, err)
  248. req.Header.Set("Content-Type", tt.contentType)
  249. rr := httptest.NewRecorder()
  250. handler.ClientLogin(rr, req)
  251. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  252. responseBody := strings.TrimSpace(rr.Body.String())
  253. if tt.checkResponse != nil {
  254. tt.checkResponse(t, responseBody)
  255. }
  256. })
  257. }
  258. }