presence_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/mock"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // MockFeedbagRetriever is a mock implementation of FeedbagRetriever
  15. type MockFeedbagRetriever struct {
  16. mock.Mock
  17. }
  18. func (m *MockFeedbagRetriever) RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error) {
  19. args := m.Called(ctx, screenName)
  20. if items := args.Get(0); items != nil {
  21. return items.([]wire.FeedbagItem), args.Error(1)
  22. }
  23. return nil, args.Error(1)
  24. }
  25. func (m *MockFeedbagRetriever) RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error) {
  26. args := m.Called(ctx, screenName)
  27. if names := args.Get(0); names != nil {
  28. return names.([]state.IdentScreenName), args.Error(1)
  29. }
  30. return nil, args.Error(1)
  31. }
  32. // MockBuddyBroadcaster is a mock implementation of BuddyBroadcaster
  33. type MockBuddyBroadcaster struct {
  34. mock.Mock
  35. }
  36. func (m *MockBuddyBroadcaster) BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error {
  37. args := m.Called(ctx, screenName, userInfo)
  38. return args.Error(0)
  39. }
  40. func (m *MockBuddyBroadcaster) BroadcastBuddyDeparted(ctx context.Context, instance *state.SessionInstance) error {
  41. args := m.Called(ctx, instance)
  42. return args.Error(0)
  43. }
  44. // MockProfileManager is a mock implementation of ProfileManager
  45. type MockProfileManager struct {
  46. mock.Mock
  47. }
  48. func (m *MockProfileManager) SetProfile(ctx context.Context, screenName state.IdentScreenName, profile state.UserProfile) error {
  49. args := m.Called(ctx, screenName, profile)
  50. return args.Error(0)
  51. }
  52. func (m *MockProfileManager) Profile(ctx context.Context, screenName state.IdentScreenName) (state.UserProfile, error) {
  53. args := m.Called(ctx, screenName)
  54. return args.Get(0).(state.UserProfile), args.Error(1)
  55. }
  56. func TestPresenceHandler_GetPresence(t *testing.T) {
  57. tests := []struct {
  58. name string
  59. queryParams string
  60. setupMocks func(*MockSessionRetriever, *MockFeedbagRetriever, *MockRelationshipFetcher)
  61. expectedStatusCode int
  62. checkResponse func(*testing.T, string)
  63. }{
  64. {
  65. name: "Success_BuddyList",
  66. queryParams: "bl=1",
  67. setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
  68. // Return feedbag with a group and buddy
  69. fr.On("RetrieveFeedbag", mock.Anything, state.NewIdentScreenName("testuser")).
  70. Return([]wire.FeedbagItem{
  71. {ItemID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends", GroupID: 0},
  72. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "buddy1", GroupID: 1},
  73. }, nil)
  74. rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("buddy1")).
  75. Return(state.Relationship{}, nil)
  76. sr.On("RetrieveSession", state.NewIdentScreenName("buddy1")).
  77. Return(nil)
  78. },
  79. expectedStatusCode: http.StatusOK,
  80. checkResponse: func(t *testing.T, body string) {
  81. assert.Contains(t, body, `"statusCode":200`)
  82. assert.Contains(t, body, `"groups"`)
  83. assert.Contains(t, body, `"Friends"`)
  84. assert.Contains(t, body, `"buddy1"`)
  85. assert.Contains(t, body, `"offline"`)
  86. },
  87. },
  88. {
  89. name: "Success_TargetUsers",
  90. queryParams: "t=user1,user2",
  91. setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
  92. rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("user1")).
  93. Return(state.Relationship{}, nil)
  94. rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("user2")).
  95. Return(state.Relationship{}, nil)
  96. sr.On("RetrieveSession", state.NewIdentScreenName("user1")).
  97. Return(nil)
  98. sr.On("RetrieveSession", state.NewIdentScreenName("user2")).
  99. Return(nil)
  100. },
  101. expectedStatusCode: http.StatusOK,
  102. checkResponse: func(t *testing.T, body string) {
  103. assert.Contains(t, body, `"statusCode":200`)
  104. assert.Contains(t, body, `"users"`)
  105. assert.Contains(t, body, `"user1"`)
  106. assert.Contains(t, body, `"user2"`)
  107. },
  108. },
  109. {
  110. name: "Success_BlockedUserOffline",
  111. queryParams: "t=blockeduser",
  112. setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
  113. rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("blockeduser")).
  114. Return(state.Relationship{YouBlock: true}, nil)
  115. // RetrieveSession should NOT be called for blocked users
  116. },
  117. expectedStatusCode: http.StatusOK,
  118. checkResponse: func(t *testing.T, body string) {
  119. assert.Contains(t, body, `"statusCode":200`)
  120. assert.Contains(t, body, `"blockeduser"`)
  121. assert.Contains(t, body, `"offline"`)
  122. },
  123. },
  124. {
  125. name: "Success_EmptyRequest",
  126. queryParams: "",
  127. setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {},
  128. expectedStatusCode: http.StatusOK,
  129. checkResponse: func(t *testing.T, body string) {
  130. assert.Contains(t, body, `"statusCode":200`)
  131. },
  132. },
  133. }
  134. for _, tt := range tests {
  135. t.Run(tt.name, func(t *testing.T) {
  136. sessionRetriever := &MockSessionRetriever{}
  137. feedbagRetriever := &MockFeedbagRetriever{}
  138. relFetcher := &MockRelationshipFetcher{}
  139. sessionMgr, aimsid := createTestSessionManager("testuser")
  140. handler := &PresenceHandler{
  141. SessionManager: sessionMgr,
  142. SessionRetriever: sessionRetriever,
  143. FeedbagRetriever: feedbagRetriever,
  144. RelationshipFetcher: relFetcher,
  145. Logger: slog.Default(),
  146. }
  147. tt.setupMocks(sessionRetriever, feedbagRetriever, relFetcher)
  148. reqURL := "/presence/get?aimsid=" + aimsid
  149. if tt.queryParams != "" {
  150. reqURL += "&" + tt.queryParams
  151. }
  152. req, err := http.NewRequest("GET", reqURL, nil)
  153. assert.NoError(t, err)
  154. rr := httptest.NewRecorder()
  155. handler.GetPresence(rr, req)
  156. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  157. if tt.checkResponse != nil {
  158. responseBody := strings.TrimSpace(rr.Body.String())
  159. tt.checkResponse(t, responseBody)
  160. }
  161. sessionRetriever.AssertExpectations(t)
  162. feedbagRetriever.AssertExpectations(t)
  163. relFetcher.AssertExpectations(t)
  164. })
  165. }
  166. }
  167. func TestPresenceHandler_GetPresence_MissingAimsid(t *testing.T) {
  168. handler := &PresenceHandler{
  169. SessionManager: state.NewWebAPISessionManager(),
  170. Logger: slog.Default(),
  171. }
  172. req, err := http.NewRequest("GET", "/presence/get", nil)
  173. assert.NoError(t, err)
  174. rr := httptest.NewRecorder()
  175. handler.GetPresence(rr, req)
  176. assert.Equal(t, http.StatusBadRequest, rr.Code)
  177. assert.Contains(t, rr.Body.String(), "missing aimsid parameter")
  178. }
  179. func TestPresenceHandler_GetPresence_SessionNotFound(t *testing.T) {
  180. handler := &PresenceHandler{
  181. SessionManager: state.NewWebAPISessionManager(),
  182. Logger: slog.Default(),
  183. }
  184. req, err := http.NewRequest("GET", "/presence/get?aimsid=nonexistent", nil)
  185. assert.NoError(t, err)
  186. rr := httptest.NewRecorder()
  187. handler.GetPresence(rr, req)
  188. assert.Equal(t, http.StatusNotFound, rr.Code)
  189. assert.Contains(t, rr.Body.String(), "session not found")
  190. }
  191. func TestPresenceHandler_SetState_MissingAimsid(t *testing.T) {
  192. handler := &PresenceHandler{
  193. SessionManager: state.NewWebAPISessionManager(),
  194. Logger: slog.Default(),
  195. }
  196. req, err := http.NewRequest("GET", "/presence/setState", nil)
  197. assert.NoError(t, err)
  198. rr := httptest.NewRecorder()
  199. handler.SetState(rr, req)
  200. assert.Equal(t, http.StatusBadRequest, rr.Code)
  201. assert.Contains(t, rr.Body.String(), "missing aimsid parameter")
  202. }
  203. func TestPresenceHandler_SetState_InvalidState(t *testing.T) {
  204. sessionMgr, aimsid := createTestSessionManager("testuser")
  205. handler := &PresenceHandler{
  206. SessionManager: sessionMgr,
  207. Logger: slog.Default(),
  208. }
  209. req, err := http.NewRequest("GET", "/presence/setState?aimsid="+aimsid+"&state=bogus", nil)
  210. assert.NoError(t, err)
  211. rr := httptest.NewRecorder()
  212. handler.SetState(rr, req)
  213. // Web-only sessions (no OSCAR session) return 200 before checking state param
  214. // because the handler returns early with success for web-only sessions
  215. assert.Equal(t, http.StatusOK, rr.Code)
  216. }
  217. func TestPresenceHandler_SetState_WebOnlySession(t *testing.T) {
  218. // Web-only sessions (no OSCAR session) should return success
  219. sessionMgr, aimsid := createTestSessionManager("testuser")
  220. handler := &PresenceHandler{
  221. SessionManager: sessionMgr,
  222. Logger: slog.Default(),
  223. }
  224. req, err := http.NewRequest("GET", "/presence/setState?aimsid="+aimsid+"&state=online", nil)
  225. assert.NoError(t, err)
  226. rr := httptest.NewRecorder()
  227. handler.SetState(rr, req)
  228. assert.Equal(t, http.StatusOK, rr.Code)
  229. assert.Contains(t, rr.Body.String(), `"statusCode":200`)
  230. }
  231. func TestIsICQScreenName(t *testing.T) {
  232. tests := []struct {
  233. name string
  234. screenName string
  235. expected bool
  236. }{
  237. {"ICQ_Number", "123456789", true},
  238. {"AIM_Name", "cooluser", false},
  239. {"AIM_WithNumbers", "cool123", false},
  240. {"Empty", "", false},
  241. {"Single_Digit", "5", true},
  242. {"Mixed_Chars", "12abc34", false},
  243. }
  244. for _, tt := range tests {
  245. t.Run(tt.name, func(t *testing.T) {
  246. assert.Equal(t, tt.expected, isICQScreenName(tt.screenName))
  247. })
  248. }
  249. }
  250. func TestPresenceHandler_Icon(t *testing.T) {
  251. tests := []struct {
  252. name string
  253. queryParams string
  254. setupMocks func(*MockSessionRetriever)
  255. expectedStatusCode int
  256. checkRedirect func(*testing.T, *httptest.ResponseRecorder)
  257. }{
  258. {
  259. name: "Redirect_OfflineUser",
  260. queryParams: "name=offlineuser",
  261. setupMocks: func(sr *MockSessionRetriever) {
  262. sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).Return(nil)
  263. },
  264. expectedStatusCode: http.StatusFound,
  265. checkRedirect: func(t *testing.T, rr *httptest.ResponseRecorder) {
  266. location := rr.Header().Get("Location")
  267. assert.Contains(t, location, "offline")
  268. },
  269. },
  270. {
  271. name: "Error_MissingName",
  272. queryParams: "",
  273. setupMocks: func(sr *MockSessionRetriever) {},
  274. expectedStatusCode: http.StatusBadRequest,
  275. },
  276. }
  277. for _, tt := range tests {
  278. t.Run(tt.name, func(t *testing.T) {
  279. sessionRetriever := &MockSessionRetriever{}
  280. handler := &PresenceHandler{
  281. SessionRetriever: sessionRetriever,
  282. Logger: slog.Default(),
  283. }
  284. tt.setupMocks(sessionRetriever)
  285. reqURL := "/presence/icon"
  286. if tt.queryParams != "" {
  287. reqURL += "?" + tt.queryParams
  288. }
  289. req, err := http.NewRequest("GET", reqURL, nil)
  290. assert.NoError(t, err)
  291. rr := httptest.NewRecorder()
  292. handler.Icon(rr, req)
  293. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  294. if tt.checkRedirect != nil {
  295. tt.checkRedirect(t, rr)
  296. }
  297. sessionRetriever.AssertExpectations(t)
  298. })
  299. }
  300. }
  301. func TestPresenceHandler_SetProfile(t *testing.T) {
  302. tests := []struct {
  303. name string
  304. queryParams string
  305. setupMocks func(*MockProfileManager)
  306. expectedStatusCode int
  307. checkResponse func(*testing.T, string)
  308. }{
  309. {
  310. name: "Success_SetProfile",
  311. queryParams: "profile=Hello+World",
  312. setupMocks: func(pm *MockProfileManager) {
  313. pm.On("SetProfile", mock.Anything, state.NewIdentScreenName("testuser"), mock.AnythingOfType("state.UserProfile")).Return(nil)
  314. },
  315. expectedStatusCode: http.StatusOK,
  316. checkResponse: func(t *testing.T, body string) {
  317. assert.Contains(t, body, `"statusCode":200`)
  318. },
  319. },
  320. {
  321. name: "Error_ProfileTooLarge",
  322. queryParams: "profile=" + strings.Repeat("x", 4097),
  323. setupMocks: func(pm *MockProfileManager) {},
  324. expectedStatusCode: http.StatusBadRequest,
  325. checkResponse: func(t *testing.T, body string) {
  326. assert.Contains(t, body, "profile too large")
  327. },
  328. },
  329. }
  330. for _, tt := range tests {
  331. t.Run(tt.name, func(t *testing.T) {
  332. profileManager := &MockProfileManager{}
  333. sessionMgr, aimsid := createTestSessionManager("testuser")
  334. handler := &PresenceHandler{
  335. SessionManager: sessionMgr,
  336. ProfileManager: profileManager,
  337. Logger: slog.Default(),
  338. }
  339. tt.setupMocks(profileManager)
  340. reqURL := "/presence/setProfile?aimsid=" + aimsid + "&" + tt.queryParams
  341. req, err := http.NewRequest("GET", reqURL, nil)
  342. assert.NoError(t, err)
  343. rr := httptest.NewRecorder()
  344. handler.SetProfile(rr, req)
  345. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  346. if tt.checkResponse != nil {
  347. responseBody := strings.TrimSpace(rr.Body.String())
  348. tt.checkResponse(t, responseBody)
  349. }
  350. profileManager.AssertExpectations(t)
  351. })
  352. }
  353. }
  354. func TestPresenceHandler_GetProfile(t *testing.T) {
  355. profileManager := &MockProfileManager{}
  356. sessionMgr, aimsid := createTestSessionManager("testuser")
  357. handler := &PresenceHandler{
  358. SessionManager: sessionMgr,
  359. ProfileManager: profileManager,
  360. Logger: slog.Default(),
  361. }
  362. profileManager.On("Profile", mock.Anything, state.NewIdentScreenName("testuser")).
  363. Return(state.UserProfile{ProfileText: "My profile"}, nil)
  364. req, err := http.NewRequest("GET", "/presence/getProfile?aimsid="+aimsid, nil)
  365. assert.NoError(t, err)
  366. rr := httptest.NewRecorder()
  367. handler.GetProfile(rr, req)
  368. assert.Equal(t, http.StatusOK, rr.Code)
  369. body := rr.Body.String()
  370. assert.Contains(t, body, `"statusCode":200`)
  371. assert.Contains(t, body, `"My profile"`)
  372. assert.Contains(t, body, `"testuser"`)
  373. profileManager.AssertExpectations(t)
  374. }