preference_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/mock"
  10. "github.com/mk6i/open-oscar-server/server/webapi/types"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // buddyPrefsFeedbag builds a feedbag reply carrying a single buddy-prefs item
  15. // with the given prefs pre-set.
  16. func buddyPrefsFeedbag(prefs map[uint16]bool) wire.SNACMessage {
  17. var list wire.TLVList
  18. for num, val := range prefs {
  19. list = wire.SetBuddyPref(list, num, val)
  20. }
  21. return wire.SNACMessage{
  22. Body: wire.SNAC_0x13_0x06_FeedbagReply{
  23. Items: []wire.FeedbagItem{
  24. {ClassID: wire.FeedbagClassIdBuddyPrefs, ItemID: 1, TLVLBlock: wire.TLVLBlock{TLVList: list}},
  25. },
  26. },
  27. }
  28. }
  29. func TestPreferenceHandler_SetPreferences(t *testing.T) {
  30. fs := &MockFeedbagService{}
  31. oscarInstance := state.NewSession().AddInstance()
  32. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  33. // Existing feedbag already has acceptCustomBart (0x0B, default false) enabled;
  34. // it must survive the read-modify-write. Using a default-false pref means an
  35. // observed true value can only come from the stored bit, not the default.
  36. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  37. Return(buddyPrefsFeedbag(map[uint16]bool{wire.FeedbagBuddyPrefsAcceptCustomBart: true}), nil)
  38. var upserted []wire.FeedbagItem
  39. fs.On("UpsertItem", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
  40. Run(func(args mock.Arguments) { upserted = args.Get(3).([]wire.FeedbagItem) }).
  41. Return(nil, nil)
  42. handler := &PreferenceHandler{
  43. SessionManager: sessionMgr,
  44. FeedbagService: fs,
  45. Logger: slog.Default(),
  46. }
  47. req, _ := http.NewRequest("GET", "/preference/set?aimsid="+aimsid+"&playIMSound=0&discloseTyping=1", nil)
  48. rr := httptest.NewRecorder()
  49. requireSession(handler.SessionManager, handler.SetPreferences).ServeHTTP(rr, req)
  50. assert.Equal(t, http.StatusOK, rr.Code)
  51. if assert.Len(t, upserted, 1) {
  52. item := upserted[0]
  53. assert.Equal(t, wire.FeedbagClassIdBuddyPrefs, item.ClassID)
  54. assertPref := func(num uint16, want bool) {
  55. assert.Equalf(t, want, wire.BuddyPref(item.TLVList, num), "pref 0x%02x", num)
  56. }
  57. assertPref(wire.FeedbagBuddyPrefsAcceptCustomBart, true) // preserved (default false)
  58. assertPref(0x15, false) // playIMSound off (default true)
  59. assertPref(0x16, true) // discloseTyping on
  60. }
  61. fs.AssertExpectations(t)
  62. }
  63. func TestPreferenceHandler_GetPreferences_Selected(t *testing.T) {
  64. fs := &MockFeedbagService{}
  65. oscarInstance := state.NewSession().AddInstance()
  66. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  67. // playIMSound (0x15) explicitly disabled in the feedbag.
  68. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  69. Return(buddyPrefsFeedbag(map[uint16]bool{0x15: false}), nil)
  70. handler := &PreferenceHandler{
  71. SessionManager: sessionMgr,
  72. FeedbagService: fs,
  73. Logger: slog.Default(),
  74. }
  75. // Request two prefs: playIMSound (stored=false) and acceptIcons (unset -> default true).
  76. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid+"&playIMSound&acceptIcons", nil)
  77. rr := httptest.NewRecorder()
  78. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  79. assert.Equal(t, http.StatusOK, rr.Code)
  80. body := rr.Body.String()
  81. assert.Contains(t, body, `"playIMSound":0`)
  82. assert.Contains(t, body, `"acceptIcons":1`)
  83. // Only the two requested prefs should be present.
  84. assert.NotContains(t, body, `"displayLogin"`)
  85. }
  86. func TestPreferenceHandler_GetPreferences_All(t *testing.T) {
  87. fs := &MockFeedbagService{}
  88. oscarInstance := state.NewSession().AddInstance()
  89. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  90. // Empty feedbag -> every pref resolves to its spec default.
  91. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  92. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{}}, nil)
  93. handler := &PreferenceHandler{
  94. SessionManager: sessionMgr,
  95. FeedbagService: fs,
  96. Logger: slog.Default(),
  97. }
  98. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid, nil)
  99. rr := httptest.NewRecorder()
  100. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  101. assert.Equal(t, http.StatusOK, rr.Code)
  102. body := rr.Body.String()
  103. assert.Contains(t, body, `"displayLogin":1`) // default true
  104. assert.Contains(t, body, `"acceptCustomBart":0`) // default false
  105. }
  106. func TestPreferenceHandler_GetPreferences_AMF(t *testing.T) {
  107. fs := &MockFeedbagService{}
  108. oscarInstance := state.NewSession().AddInstance()
  109. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  110. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  111. Return(buddyPrefsFeedbag(map[uint16]bool{0x15: false}), nil)
  112. handler := &PreferenceHandler{
  113. SessionManager: sessionMgr,
  114. FeedbagService: fs,
  115. Logger: slog.Default(),
  116. }
  117. // Single-pref AMF request returns a numeric value (not wrapped in jsonData).
  118. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid+"&f=amf&playIMSound", nil)
  119. rr := httptest.NewRecorder()
  120. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  121. assert.Equal(t, http.StatusOK, rr.Code)
  122. body := rr.Body.String()
  123. // AMF-encoded response: the value is a numeric 0 (AMF integer marker 0x04
  124. // followed by 0x00), not the string "0".
  125. assert.Contains(t, body, "playIMSound\x04\x00")
  126. assert.NotContains(t, body, `playIMSound":"0"`)
  127. }
  128. func TestEffectiveBuddyPrefs_StoredOverridesDefault(t *testing.T) {
  129. // playIMSound defaults true but is stored false; viewIMsInBubbles defaults
  130. // true and is stored true. Stored (valid) values must win over defaults.
  131. var list wire.TLVList
  132. list = wire.SetBuddyPref(list, wire.FeedbagBuddyPrefsPlayIMSound, false)
  133. list = wire.SetBuddyPref(list, wire.FeedbagBuddyPrefsViewIMsInBubbles, true)
  134. got := effectiveBuddyPrefs(list)
  135. // Every pref is present (defaults applied for unset ones).
  136. assert.Len(t, got, len(webBuddyPrefs))
  137. assert.Equal(t, 0, got["playIMSound"])
  138. assert.Equal(t, 1, got["viewIMsInBubbles"])
  139. }
  140. func TestEffectiveBuddyPrefs_AppliesDefaultsWhenNothingSet(t *testing.T) {
  141. got := effectiveBuddyPrefs(wire.TLVList{})
  142. // Unset prefs resolve to their spec defaults rather than being omitted.
  143. assert.Len(t, got, len(webBuddyPrefs))
  144. assert.Equal(t, 1, got["showGroups"], "showGroups should default to shown")
  145. assert.Equal(t, 1, got["playIMSound"], "playIMSound defaults true")
  146. assert.Equal(t, 0, got["sortBuddyList"], "sortBuddyList defaults false")
  147. }
  148. func TestPreferenceHandler_SetPermitDeny_QueuesPermitDenyEvent(t *testing.T) {
  149. // The client renders blocked buddies from the permitDeny event alone, and it
  150. // sees no SNAC for its own write, so the handler has to queue the new state.
  151. fs := &MockFeedbagService{}
  152. oscarInstance := state.NewSession().AddInstance()
  153. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  154. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  155. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{}}, nil)
  156. fs.On("UpsertItem", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
  157. Return(nil, nil)
  158. handler := &PreferenceHandler{
  159. SessionManager: sessionMgr,
  160. FeedbagService: fs,
  161. Logger: slog.Default(),
  162. }
  163. req, _ := http.NewRequest("GET", "/preference/setPermitDeny?aimsid="+aimsid+"&pdMode=denySome&pdBlock=BlockedUser", nil)
  164. rr := httptest.NewRecorder()
  165. requireSession(handler.SessionManager, handler.SetPermitDeny).ServeHTTP(rr, req)
  166. assert.Equal(t, http.StatusOK, rr.Code)
  167. session, err := sessionMgr.GetSession(context.Background(), aimsid)
  168. assert.NoError(t, err)
  169. var pdd PermitDenyData
  170. var found bool
  171. for _, event := range session.EventQueue.GetAllEvents() {
  172. if event.Type == types.EventTypePermitDeny {
  173. pdd, found = event.Data.(PermitDenyData)
  174. }
  175. }
  176. assert.True(t, found, "expected a permitDeny event to be queued")
  177. assert.Equal(t, "denySome", pdd.PDMode)
  178. assert.Equal(t, []string{"blockeduser"}, pdd.DenyList)
  179. }
  180. func TestPermitDenyData_DefaultsToPermitAllWithoutPDInfo(t *testing.T) {
  181. got := permitDenyData([]wire.FeedbagItem{
  182. {ClassID: wire.FeedbagClassIDDeny, Name: "blockeduser"},
  183. })
  184. assert.Equal(t, "permitAll", got.PDMode)
  185. assert.Equal(t, []string{"blockeduser"}, got.DenyList)
  186. }
  187. func TestPermitDenyData_PDInfoModeWins(t *testing.T) {
  188. var tlvs wire.TLVList
  189. tlvs.Append(wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(wire.FeedbagPDModePermitSome)))
  190. got := permitDenyData([]wire.FeedbagItem{
  191. {ClassID: wire.FeedbagClassIdPdinfo, TLVLBlock: wire.TLVLBlock{TLVList: tlvs}},
  192. {ClassID: wire.FeedbagClassIDPermit, Name: "alloweduser"},
  193. })
  194. assert.Equal(t, "permitSome", got.PDMode)
  195. assert.Equal(t, []string{"alloweduser"}, got.PermitList)
  196. }
  197. func TestPreferenceHandler_SetPreferences_NoOSCARSession(t *testing.T) {
  198. fs := &MockFeedbagService{}
  199. sessionMgr, aimsid := createTestSessionManager("webonly") // nil OSCARSession
  200. handler := &PreferenceHandler{
  201. SessionManager: sessionMgr,
  202. FeedbagService: fs,
  203. Logger: slog.Default(),
  204. }
  205. req, _ := http.NewRequest("GET", "/preference/set?aimsid="+aimsid+"&playIMSound=1", nil)
  206. rr := httptest.NewRecorder()
  207. requireSession(handler.SessionManager, handler.SetPreferences).ServeHTTP(rr, req)
  208. // A nil OSCARSession is a broken server invariant (guests are unsupported),
  209. // so the session middleware rejects it with a 500 before the handler runs
  210. // and no feedbag lookup occurs.
  211. assert.Equal(t, http.StatusInternalServerError, rr.Code)
  212. assert.Contains(t, rr.Body.String(), "internal server error")
  213. fs.AssertNotCalled(t, "Query", mock.Anything, mock.Anything, mock.Anything)
  214. }