preference_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package handlers
  2. import (
  3. "log/slog"
  4. "net/http"
  5. "net/http/httptest"
  6. "testing"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/mock"
  9. "github.com/mk6i/open-oscar-server/state"
  10. "github.com/mk6i/open-oscar-server/wire"
  11. )
  12. // buddyPrefsFeedbag builds a feedbag reply carrying a single buddy-prefs item
  13. // with the given prefs pre-set.
  14. func buddyPrefsFeedbag(prefs map[uint16]bool) wire.SNACMessage {
  15. var list wire.TLVList
  16. for num, val := range prefs {
  17. list = wire.SetBuddyPref(list, num, val)
  18. }
  19. return wire.SNACMessage{
  20. Body: wire.SNAC_0x13_0x06_FeedbagReply{
  21. Items: []wire.FeedbagItem{
  22. {ClassID: wire.FeedbagClassIdBuddyPrefs, ItemID: 1, TLVLBlock: wire.TLVLBlock{TLVList: list}},
  23. },
  24. },
  25. }
  26. }
  27. func TestPreferenceHandler_SetPreferences(t *testing.T) {
  28. fs := &MockFeedbagService{}
  29. oscarInstance := state.NewSession().AddInstance()
  30. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  31. // Existing feedbag already has acceptCustomBart (0x0B, default false) enabled;
  32. // it must survive the read-modify-write. Using a default-false pref means an
  33. // observed true value can only come from the stored bit, not the default.
  34. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  35. Return(buddyPrefsFeedbag(map[uint16]bool{wire.FeedbagBuddyPrefsAcceptCustomBart: true}), nil)
  36. var upserted []wire.FeedbagItem
  37. fs.On("UpsertItem", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
  38. Run(func(args mock.Arguments) { upserted = args.Get(3).([]wire.FeedbagItem) }).
  39. Return(nil, nil)
  40. handler := &PreferenceHandler{
  41. SessionManager: sessionMgr,
  42. FeedbagService: fs,
  43. Logger: slog.Default(),
  44. }
  45. req, _ := http.NewRequest("GET", "/preference/set?aimsid="+aimsid+"&playIMSound=0&discloseTyping=1", nil)
  46. rr := httptest.NewRecorder()
  47. requireSession(handler.SessionManager, handler.SetPreferences).ServeHTTP(rr, req)
  48. assert.Equal(t, http.StatusOK, rr.Code)
  49. if assert.Len(t, upserted, 1) {
  50. item := upserted[0]
  51. assert.Equal(t, wire.FeedbagClassIdBuddyPrefs, item.ClassID)
  52. assertPref := func(num uint16, want bool) {
  53. assert.Equalf(t, want, wire.BuddyPref(item.TLVList, num), "pref 0x%02x", num)
  54. }
  55. assertPref(wire.FeedbagBuddyPrefsAcceptCustomBart, true) // preserved (default false)
  56. assertPref(0x15, false) // playIMSound off (default true)
  57. assertPref(0x16, true) // discloseTyping on
  58. }
  59. fs.AssertExpectations(t)
  60. }
  61. func TestPreferenceHandler_GetPreferences_Selected(t *testing.T) {
  62. fs := &MockFeedbagService{}
  63. oscarInstance := state.NewSession().AddInstance()
  64. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  65. // playIMSound (0x15) explicitly disabled in the feedbag.
  66. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  67. Return(buddyPrefsFeedbag(map[uint16]bool{0x15: false}), nil)
  68. handler := &PreferenceHandler{
  69. SessionManager: sessionMgr,
  70. FeedbagService: fs,
  71. Logger: slog.Default(),
  72. }
  73. // Request two prefs: playIMSound (stored=false) and acceptIcons (unset -> default true).
  74. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid+"&playIMSound&acceptIcons", nil)
  75. rr := httptest.NewRecorder()
  76. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  77. assert.Equal(t, http.StatusOK, rr.Code)
  78. body := rr.Body.String()
  79. assert.Contains(t, body, `"playIMSound":0`)
  80. assert.Contains(t, body, `"acceptIcons":1`)
  81. // Only the two requested prefs should be present.
  82. assert.NotContains(t, body, `"displayLogin"`)
  83. }
  84. func TestPreferenceHandler_GetPreferences_All(t *testing.T) {
  85. fs := &MockFeedbagService{}
  86. oscarInstance := state.NewSession().AddInstance()
  87. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  88. // Empty feedbag -> every pref resolves to its spec default.
  89. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  90. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{}}, nil)
  91. handler := &PreferenceHandler{
  92. SessionManager: sessionMgr,
  93. FeedbagService: fs,
  94. Logger: slog.Default(),
  95. }
  96. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid, nil)
  97. rr := httptest.NewRecorder()
  98. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  99. assert.Equal(t, http.StatusOK, rr.Code)
  100. body := rr.Body.String()
  101. assert.Contains(t, body, `"displayLogin":1`) // default true
  102. assert.Contains(t, body, `"acceptCustomBart":0`) // default false
  103. }
  104. func TestPreferenceHandler_GetPreferences_AMF(t *testing.T) {
  105. fs := &MockFeedbagService{}
  106. oscarInstance := state.NewSession().AddInstance()
  107. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  108. fs.On("Query", mock.Anything, oscarInstance, mock.Anything).
  109. Return(buddyPrefsFeedbag(map[uint16]bool{0x15: false}), nil)
  110. handler := &PreferenceHandler{
  111. SessionManager: sessionMgr,
  112. FeedbagService: fs,
  113. Logger: slog.Default(),
  114. }
  115. // Single-pref AMF request returns a numeric value (not wrapped in jsonData).
  116. req, _ := http.NewRequest("GET", "/preference/get?aimsid="+aimsid+"&f=amf&playIMSound", nil)
  117. rr := httptest.NewRecorder()
  118. requireSession(handler.SessionManager, handler.GetPreferences).ServeHTTP(rr, req)
  119. assert.Equal(t, http.StatusOK, rr.Code)
  120. body := rr.Body.String()
  121. // AMF-encoded response: the value is a numeric 0 (AMF integer marker 0x04
  122. // followed by 0x00), not the string "0".
  123. assert.Contains(t, body, "playIMSound\x04\x00")
  124. assert.NotContains(t, body, `playIMSound":"0"`)
  125. }
  126. func TestEffectiveBuddyPrefs_StoredOverridesDefault(t *testing.T) {
  127. // playIMSound defaults true but is stored false; viewIMsInBubbles defaults
  128. // true and is stored true. Stored (valid) values must win over defaults.
  129. var list wire.TLVList
  130. list = wire.SetBuddyPref(list, wire.FeedbagBuddyPrefsPlayIMSound, false)
  131. list = wire.SetBuddyPref(list, wire.FeedbagBuddyPrefsViewIMsInBubbles, true)
  132. got := effectiveBuddyPrefs(list)
  133. // Every pref is present (defaults applied for unset ones).
  134. assert.Len(t, got, len(webBuddyPrefs))
  135. assert.Equal(t, 0, got["playIMSound"])
  136. assert.Equal(t, 1, got["viewIMsInBubbles"])
  137. }
  138. func TestEffectiveBuddyPrefs_AppliesDefaultsWhenNothingSet(t *testing.T) {
  139. got := effectiveBuddyPrefs(wire.TLVList{})
  140. // Unset prefs resolve to their spec defaults rather than being omitted.
  141. assert.Len(t, got, len(webBuddyPrefs))
  142. assert.Equal(t, 1, got["showGroups"], "showGroups should default to shown")
  143. assert.Equal(t, 1, got["playIMSound"], "playIMSound defaults true")
  144. assert.Equal(t, 0, got["sortBuddyList"], "sortBuddyList defaults false")
  145. }
  146. func TestPreferenceHandler_SetPreferences_NoOSCARSession(t *testing.T) {
  147. fs := &MockFeedbagService{}
  148. sessionMgr, aimsid := createTestSessionManager("webonly") // nil OSCARSession
  149. handler := &PreferenceHandler{
  150. SessionManager: sessionMgr,
  151. FeedbagService: fs,
  152. Logger: slog.Default(),
  153. }
  154. req, _ := http.NewRequest("GET", "/preference/set?aimsid="+aimsid+"&playIMSound=1", nil)
  155. rr := httptest.NewRecorder()
  156. requireSession(handler.SessionManager, handler.SetPreferences).ServeHTTP(rr, req)
  157. // A nil OSCARSession is a broken server invariant (guests are unsupported),
  158. // so the session middleware rejects it with a 500 before the handler runs
  159. // and no feedbag lookup occurs.
  160. assert.Equal(t, http.StatusInternalServerError, rr.Code)
  161. assert.Contains(t, rr.Body.String(), "internal server error")
  162. fs.AssertNotCalled(t, "Query", mock.Anything, mock.Anything, mock.Anything)
  163. }