auth_test.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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. type testUserRetriever struct {
  62. user *state.User
  63. err error
  64. }
  65. func (t *testUserRetriever) User(ctx context.Context, screenName state.IdentScreenName) (*state.User, error) {
  66. if t.err != nil {
  67. return nil, t.err
  68. }
  69. return t.user, nil
  70. }
  71. func TestAuthHandler_GetToken(t *testing.T) {
  72. tests := []struct {
  73. name string
  74. query string
  75. cookies []*http.Cookie
  76. user *state.User
  77. checkBody func(*testing.T, string)
  78. expectedCode int
  79. }{
  80. {
  81. name: "Success_LocalAuthUserCookie",
  82. query: "f=json&attributes=loginId&devId=ao1yOLlHVHhsa3o6&c=_callbacks_._0mq8wqdav",
  83. cookies: []*http.Cookie{
  84. {Name: "localAuthUser", Value: "testuser||Test User"},
  85. },
  86. user: &state.User{},
  87. checkBody: func(t *testing.T, body string) {
  88. assert.Contains(t, body, "_callbacks_._0mq8wqdav(")
  89. assert.Contains(t, body, `"statusCode":200`)
  90. assert.Contains(t, body, `"loginId":"testuser"`)
  91. assert.Contains(t, body, `"a":`)
  92. },
  93. expectedCode: http.StatusOK,
  94. },
  95. {
  96. name: "Unauthorized_NoSession",
  97. query: "f=json&attributes=loginId&devId=ao1yOLlHVHhsa3o6&c=_callbacks_._abc",
  98. checkBody: func(t *testing.T, body string) {
  99. assert.Contains(t, body, `"statusCode":401`)
  100. assert.Contains(t, body, `"redirectURL"`)
  101. },
  102. expectedCode: http.StatusOK,
  103. },
  104. {
  105. name: "Unauthorized_UnknownUser",
  106. query: "f=json&attributes=loginId&devId=dev123",
  107. cookies: []*http.Cookie{
  108. {Name: "localAuthUser", Value: "missing||Missing User"},
  109. },
  110. user: nil,
  111. checkBody: func(t *testing.T, body string) {
  112. assert.Contains(t, body, `"statusCode":401`)
  113. assert.Contains(t, body, `"redirectURL"`)
  114. },
  115. expectedCode: http.StatusOK,
  116. },
  117. }
  118. for _, tt := range tests {
  119. t.Run(tt.name, func(t *testing.T) {
  120. handler := &AuthHandler{
  121. AuthService: &testAuthService{},
  122. CookieBaker: &testCookieBaker{},
  123. UserManager: &testUserRetriever{user: tt.user},
  124. Logger: slog.Default(),
  125. }
  126. req, err := http.NewRequest(http.MethodGet, "/auth/getToken?"+tt.query, nil)
  127. assert.NoError(t, err)
  128. for _, c := range tt.cookies {
  129. req.AddCookie(c)
  130. }
  131. rr := httptest.NewRecorder()
  132. handler.GetToken(rr, req)
  133. assert.Equal(t, tt.expectedCode, rr.Code)
  134. if tt.checkBody != nil {
  135. tt.checkBody(t, rr.Body.String())
  136. }
  137. })
  138. }
  139. }
  140. func TestAuthHandler_ClientLogin(t *testing.T) {
  141. tests := []struct {
  142. name string
  143. method string
  144. contentType string
  145. body string
  146. auth *testAuthService
  147. expectedStatusCode int
  148. checkResponse func(*testing.T, string)
  149. }{
  150. {
  151. name: "Success_JSONBody",
  152. method: "POST",
  153. contentType: "application/json",
  154. body: `{"username":"testuser","password":"testpass","devId":"dev123"}`,
  155. auth: &testAuthService{
  156. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  157. return successfulLoginBlock(), nil
  158. },
  159. },
  160. expectedStatusCode: http.StatusOK,
  161. checkResponse: func(t *testing.T, body string) {
  162. assert.Contains(t, body, `"statusCode":200`)
  163. assert.Contains(t, body, `"loginId":"testuser"`)
  164. assert.Contains(t, body, `"screenName":"testuser"`)
  165. assert.Contains(t, body, `"token"`)
  166. assert.Contains(t, body, `"sessionSecret"`)
  167. },
  168. },
  169. {
  170. name: "Success_FormEncoded",
  171. method: "POST",
  172. contentType: "application/x-www-form-urlencoded",
  173. body: "s=testuser&pwd=testpass&devId=dev123",
  174. auth: &testAuthService{
  175. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  176. return successfulLoginBlock(), nil
  177. },
  178. },
  179. expectedStatusCode: http.StatusOK,
  180. checkResponse: func(t *testing.T, body string) {
  181. assert.Contains(t, body, `"statusCode":200`)
  182. assert.Contains(t, body, `"loginId":"testuser"`)
  183. },
  184. },
  185. {
  186. name: "Error_MissingUsername",
  187. method: "POST",
  188. contentType: "application/json",
  189. body: `{"username":"","password":"testpass"}`,
  190. auth: &testAuthService{},
  191. expectedStatusCode: http.StatusBadRequest,
  192. checkResponse: func(t *testing.T, body string) {
  193. assert.Contains(t, body, "username and password required")
  194. },
  195. },
  196. {
  197. name: "Error_MissingPassword",
  198. method: "POST",
  199. contentType: "application/json",
  200. body: `{"username":"testuser","password":""}`,
  201. auth: &testAuthService{},
  202. expectedStatusCode: http.StatusBadRequest,
  203. checkResponse: func(t *testing.T, body string) {
  204. assert.Contains(t, body, "username and password required")
  205. },
  206. },
  207. {
  208. name: "Error_AuthFailed",
  209. method: "POST",
  210. contentType: "application/json",
  211. body: `{"username":"testuser","password":"wrongpass"}`,
  212. auth: &testAuthService{
  213. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  214. return failedLoginBlock(), nil
  215. },
  216. },
  217. expectedStatusCode: http.StatusUnauthorized,
  218. checkResponse: func(t *testing.T, body string) {
  219. assert.Contains(t, body, "username and password required")
  220. },
  221. },
  222. {
  223. name: "Error_FLAPLoginError",
  224. method: "POST",
  225. contentType: "application/json",
  226. body: `{"username":"testuser","password":"testpass"}`,
  227. auth: &testAuthService{
  228. flapLogin: func(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
  229. return wire.TLVRestBlock{}, errors.New("boom")
  230. },
  231. },
  232. expectedStatusCode: http.StatusInternalServerError,
  233. checkResponse: func(t *testing.T, body string) {
  234. assert.Contains(t, body, "internal server error")
  235. },
  236. },
  237. {
  238. name: "Error_InvalidJSON",
  239. method: "POST",
  240. contentType: "application/json",
  241. body: `{invalid json`,
  242. auth: &testAuthService{},
  243. expectedStatusCode: http.StatusBadRequest,
  244. checkResponse: func(t *testing.T, body string) {
  245. assert.Contains(t, body, "invalid JSON format")
  246. },
  247. },
  248. }
  249. for _, tt := range tests {
  250. t.Run(tt.name, func(t *testing.T) {
  251. logger := slog.Default()
  252. handler := &AuthHandler{
  253. AuthService: tt.auth,
  254. Logger: logger,
  255. }
  256. req, err := http.NewRequest(tt.method, "/auth/clientLogin", strings.NewReader(tt.body))
  257. assert.NoError(t, err)
  258. req.Header.Set("Content-Type", tt.contentType)
  259. rr := httptest.NewRecorder()
  260. handler.ClientLogin(rr, req)
  261. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  262. responseBody := strings.TrimSpace(rr.Body.String())
  263. if tt.checkResponse != nil {
  264. tt.checkResponse(t, responseBody)
  265. }
  266. })
  267. }
  268. }