feedbag_list_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. package state
  2. import (
  3. "math"
  4. "testing"
  5. "github.com/mk6i/open-oscar-server/wire"
  6. "github.com/stretchr/testify/assert"
  7. )
  8. func TestFeedbagList_upsertItem(t *testing.T) {
  9. t.Run("generates unique ItemID", func(t *testing.T) {
  10. fl := NewFeedbagList([]wire.FeedbagItem{
  11. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  12. }, func(n int) int { return 42 })
  13. result, inserted := fl.upsertItem(wire.FeedbagItem{
  14. Name: "alice",
  15. ClassID: wire.FeedbagClassIdBuddy,
  16. GroupID: 1,
  17. })
  18. assert.True(t, inserted)
  19. assert.Equal(t, uint16(42), result.ItemID)
  20. assert.Equal(t, "alice", result.Name)
  21. assert.Equal(t, wire.FeedbagClassIdBuddy, result.ClassID)
  22. assert.Equal(t, uint16(1), result.GroupID)
  23. })
  24. t.Run("subsequent insert avoids collision", func(t *testing.T) {
  25. fl := NewFeedbagList([]wire.FeedbagItem{
  26. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  27. }, func(n int) int { return 42 })
  28. fl.upsertItem(wire.FeedbagItem{
  29. Name: "alice",
  30. ClassID: wire.FeedbagClassIdBuddy,
  31. GroupID: 1,
  32. })
  33. result, inserted := fl.upsertItem(wire.FeedbagItem{
  34. Name: "bob",
  35. ClassID: wire.FeedbagClassIdBuddy,
  36. GroupID: 1,
  37. })
  38. assert.True(t, inserted)
  39. assert.Equal(t, uint16(43), result.ItemID)
  40. })
  41. t.Run("non-buddy item does not update group order", func(t *testing.T) {
  42. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  43. fl.upsertItem(wire.FeedbagItem{
  44. Name: "alice",
  45. ClassID: wire.FeedbagClassIDPermit,
  46. })
  47. upserts := fl.PendingUpdates()
  48. assert.Len(t, upserts, 1)
  49. assert.Equal(t, wire.FeedbagClassIDPermit, upserts[0].ClassID)
  50. })
  51. t.Run("updates existing non-buddy item in place", func(t *testing.T) {
  52. fl := NewFeedbagList([]wire.FeedbagItem{
  53. {
  54. Name: "alice",
  55. ClassID: wire.FeedbagClassIDPermit,
  56. ItemID: 7,
  57. TLVLBlock: wire.TLVLBlock{
  58. TLVList: wire.TLVList{
  59. wire.NewTLVBE(0x01, uint16(100)),
  60. },
  61. },
  62. },
  63. }, nil)
  64. _, inserted := fl.upsertItem(wire.FeedbagItem{
  65. Name: "alice",
  66. ClassID: wire.FeedbagClassIDPermit,
  67. TLVLBlock: wire.TLVLBlock{
  68. TLVList: wire.TLVList{
  69. wire.NewTLVBE(0x01, uint16(200)),
  70. },
  71. },
  72. })
  73. assert.False(t, inserted)
  74. upserts := fl.PendingUpdates()
  75. assert.Len(t, upserts, 1)
  76. assert.Equal(t, uint16(7), upserts[0].ItemID)
  77. val, ok := upserts[0].Uint16BE(0x01)
  78. assert.True(t, ok)
  79. assert.Equal(t, uint16(200), val)
  80. })
  81. t.Run("updates existing buddy item matched by group", func(t *testing.T) {
  82. fl := NewFeedbagList([]wire.FeedbagItem{
  83. {Name: "Group1", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  84. {Name: "Group2", ClassID: wire.FeedbagClassIdGroup, GroupID: 2},
  85. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 10},
  86. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 2, ItemID: 20},
  87. }, nil)
  88. _, inserted := fl.upsertItem(wire.FeedbagItem{
  89. Name: "alice",
  90. ClassID: wire.FeedbagClassIdBuddy,
  91. GroupID: 2,
  92. TLVLBlock: wire.TLVLBlock{
  93. TLVList: wire.TLVList{
  94. wire.NewTLVBE(0x01, uint16(999)),
  95. },
  96. },
  97. })
  98. assert.False(t, inserted)
  99. upserts := fl.PendingUpdates()
  100. assert.Len(t, upserts, 1)
  101. assert.Equal(t, uint16(2), upserts[0].GroupID)
  102. assert.Equal(t, "alice", upserts[0].Name)
  103. assert.Equal(t, uint16(20), upserts[0].ItemID)
  104. val, ok := upserts[0].Uint16BE(0x01)
  105. assert.True(t, ok)
  106. assert.Equal(t, uint16(999), val)
  107. })
  108. t.Run("skips update when existing item is identical", func(t *testing.T) {
  109. fl := NewFeedbagList([]wire.FeedbagItem{
  110. {
  111. Name: "alice",
  112. ClassID: wire.FeedbagClassIDPermit,
  113. ItemID: 7,
  114. TLVLBlock: wire.TLVLBlock{
  115. TLVList: wire.TLVList{
  116. wire.NewTLVBE(0x01, uint16(100)),
  117. },
  118. },
  119. },
  120. }, nil)
  121. _, inserted := fl.upsertItem(wire.FeedbagItem{
  122. Name: "alice",
  123. ClassID: wire.FeedbagClassIDPermit,
  124. ItemID: 7,
  125. TLVLBlock: wire.TLVLBlock{
  126. TLVList: wire.TLVList{
  127. wire.NewTLVBE(0x01, uint16(100)),
  128. },
  129. },
  130. })
  131. assert.False(t, inserted)
  132. assert.Nil(t, fl.PendingUpdates())
  133. })
  134. t.Run("normalized screen name: buddy stored with lowercase name", func(t *testing.T) {
  135. fl := NewFeedbagList([]wire.FeedbagItem{
  136. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  137. }, func(n int) int { return 10 })
  138. result, inserted := fl.upsertItem(wire.FeedbagItem{
  139. Name: "Alice",
  140. ClassID: wire.FeedbagClassIdBuddy,
  141. GroupID: 1,
  142. })
  143. assert.True(t, inserted)
  144. assert.Equal(t, "alice", result.Name)
  145. })
  146. t.Run("normalized screen name: buddy upsert with different case matches existing", func(t *testing.T) {
  147. fl := NewFeedbagList([]wire.FeedbagItem{
  148. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  149. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 99},
  150. }, nil)
  151. result, inserted := fl.upsertItem(wire.FeedbagItem{
  152. Name: "ALICE",
  153. ClassID: wire.FeedbagClassIdBuddy,
  154. GroupID: 1,
  155. })
  156. assert.False(t, inserted)
  157. assert.Equal(t, uint16(99), result.ItemID)
  158. assert.Equal(t, "alice", result.Name)
  159. })
  160. t.Run("normalized screen name: permit stored with lowercase name and spaces stripped", func(t *testing.T) {
  161. fl := NewFeedbagList(nil, func(n int) int { return 1 })
  162. result, _ := fl.upsertItem(wire.FeedbagItem{
  163. Name: " Bob Smith ",
  164. ClassID: wire.FeedbagClassIDPermit,
  165. })
  166. assert.Equal(t, "bobsmith", result.Name)
  167. })
  168. t.Run("normalized screen name: permit upsert with different case matches existing", func(t *testing.T) {
  169. fl := NewFeedbagList([]wire.FeedbagItem{
  170. {Name: "bob", ClassID: wire.FeedbagClassIDPermit, ItemID: 5},
  171. }, nil)
  172. result, inserted := fl.upsertItem(wire.FeedbagItem{
  173. Name: "BOB",
  174. ClassID: wire.FeedbagClassIDPermit,
  175. })
  176. assert.False(t, inserted)
  177. assert.Equal(t, "bob", result.Name)
  178. assert.Equal(t, uint16(5), result.ItemID)
  179. })
  180. t.Run("normalized screen name: deny upsert with different case matches existing", func(t *testing.T) {
  181. fl := NewFeedbagList([]wire.FeedbagItem{
  182. {Name: "charlie", ClassID: wire.FeedbagClassIDDeny, ItemID: 3},
  183. }, nil)
  184. result, inserted := fl.upsertItem(wire.FeedbagItem{
  185. Name: "Charlie",
  186. ClassID: wire.FeedbagClassIDDeny,
  187. })
  188. assert.False(t, inserted)
  189. assert.Equal(t, "charlie", result.Name)
  190. assert.Equal(t, uint16(3), result.ItemID)
  191. })
  192. }
  193. func TestFeedbagList_deleteItem(t *testing.T) {
  194. t.Run("non-buddy item does not update group order", func(t *testing.T) {
  195. fl := NewFeedbagList([]wire.FeedbagItem{
  196. {Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 5},
  197. }, nil)
  198. fl.deleteItem(wire.FeedbagItem{Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 5})
  199. deletes := fl.PendingDeletes()
  200. assert.Len(t, deletes, 1)
  201. assert.Nil(t, fl.PendingUpdates())
  202. })
  203. t.Run("removes item from items list", func(t *testing.T) {
  204. fl := NewFeedbagList([]wire.FeedbagItem{
  205. {Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 1},
  206. {Name: "bob", ClassID: wire.FeedbagClassIDPermit, ItemID: 2},
  207. {Name: "charlie", ClassID: wire.FeedbagClassIDPermit, ItemID: 3},
  208. }, nil)
  209. fl.deleteItem(wire.FeedbagItem{Name: "bob", ClassID: wire.FeedbagClassIDPermit, ItemID: 2})
  210. deletes := fl.PendingDeletes()
  211. assert.Len(t, deletes, 1)
  212. assert.Equal(t, "bob", deletes[0].Name)
  213. assert.Equal(t, wire.FeedbagClassIDPermit, deletes[0].ClassID)
  214. })
  215. t.Run("multiple deletes accumulate", func(t *testing.T) {
  216. fl := NewFeedbagList([]wire.FeedbagItem{
  217. {Name: "alice", ClassID: wire.FeedbagClassIDDeny, ItemID: 1},
  218. {Name: "bob", ClassID: wire.FeedbagClassIDDeny, ItemID: 2},
  219. }, nil)
  220. fl.deleteItem(wire.FeedbagItem{Name: "alice", ClassID: wire.FeedbagClassIDDeny, ItemID: 1})
  221. fl.deleteItem(wire.FeedbagItem{Name: "bob", ClassID: wire.FeedbagClassIDDeny, ItemID: 2})
  222. deletes := fl.PendingDeletes()
  223. assert.Len(t, deletes, 2)
  224. })
  225. t.Run("normalized screen name: delete buddy by different case", func(t *testing.T) {
  226. fl := NewFeedbagList([]wire.FeedbagItem{
  227. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  228. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 50},
  229. }, nil)
  230. deleted, found := fl.deleteItem(wire.FeedbagItem{
  231. Name: "ALICE",
  232. ClassID: wire.FeedbagClassIdBuddy,
  233. GroupID: 1,
  234. })
  235. assert.True(t, found)
  236. assert.Equal(t, "alice", deleted.Name)
  237. assert.Equal(t, uint16(50), deleted.ItemID)
  238. })
  239. t.Run("normalized screen name: delete permit by different case", func(t *testing.T) {
  240. fl := NewFeedbagList([]wire.FeedbagItem{
  241. {Name: "bob", ClassID: wire.FeedbagClassIDPermit, ItemID: 7},
  242. }, nil)
  243. deleted, found := fl.deleteItem(wire.FeedbagItem{Name: "Bob", ClassID: wire.FeedbagClassIDPermit})
  244. assert.True(t, found)
  245. assert.Equal(t, "bob", deleted.Name)
  246. })
  247. t.Run("normalized screen name: delete deny by different case", func(t *testing.T) {
  248. fl := NewFeedbagList([]wire.FeedbagItem{
  249. {Name: "charlie", ClassID: wire.FeedbagClassIDDeny, ItemID: 8},
  250. }, nil)
  251. deleted, found := fl.deleteItem(wire.FeedbagItem{Name: "CHARLIE", ClassID: wire.FeedbagClassIDDeny})
  252. assert.True(t, found)
  253. assert.Equal(t, "charlie", deleted.Name)
  254. })
  255. }
  256. func TestFeedbagList_genID(t *testing.T) {
  257. tt := []struct {
  258. name string
  259. randInt func(n int) int
  260. items []wire.FeedbagItem
  261. want uint16
  262. }{
  263. {
  264. name: "empty items list returns random ID",
  265. randInt: func(n int) int { return 1000 },
  266. items: []wire.FeedbagItem{},
  267. want: 1000,
  268. },
  269. {
  270. name: "finds next available ID when starting ID conflicts with ItemID",
  271. randInt: func(n int) int { return 100 },
  272. items: []wire.FeedbagItem{
  273. {ItemID: 100, GroupID: 1},
  274. {ItemID: 101, GroupID: 1},
  275. },
  276. want: 102,
  277. },
  278. {
  279. name: "finds next available ID when starting ID conflicts with GroupID",
  280. randInt: func(n int) int { return 50 },
  281. items: []wire.FeedbagItem{
  282. {ItemID: 1, GroupID: 50},
  283. {ItemID: 2, GroupID: 51},
  284. },
  285. want: 52,
  286. },
  287. {
  288. name: "wraps around and skips 0 to find next available ID",
  289. randInt: func(n int) int { return math.MaxUint16 - 2 },
  290. items: []wire.FeedbagItem{
  291. {ItemID: math.MaxUint16 - 2, GroupID: 1},
  292. {ItemID: math.MaxUint16 - 1, GroupID: 1},
  293. {ItemID: math.MaxUint16, GroupID: 1},
  294. },
  295. want: 2,
  296. },
  297. {
  298. name: "skips 0 when starting from 0 and finds next available",
  299. randInt: func(n int) int { return 0 },
  300. items: []wire.FeedbagItem{},
  301. want: 1,
  302. },
  303. {
  304. name: "returns 0 when all IDs are taken",
  305. randInt: func(n int) int { return 100 },
  306. items: func() []wire.FeedbagItem {
  307. items := make([]wire.FeedbagItem, 0, math.MaxUint16+1)
  308. for i := 0; i <= math.MaxUint16; i++ {
  309. items = append(items, wire.FeedbagItem{
  310. ItemID: uint16(i),
  311. GroupID: uint16(i),
  312. })
  313. }
  314. return items
  315. }(),
  316. want: 0,
  317. },
  318. {
  319. name: "finds ID that conflicts with both ItemID and GroupID",
  320. randInt: func(n int) int { return 200 },
  321. items: []wire.FeedbagItem{
  322. {ItemID: 200, GroupID: 201},
  323. {ItemID: 201, GroupID: 200},
  324. },
  325. want: 202,
  326. },
  327. {
  328. name: "finds available ID immediately when no conflicts",
  329. randInt: func(n int) int { return 500 },
  330. items: []wire.FeedbagItem{
  331. {ItemID: 100, GroupID: 1},
  332. {ItemID: 200, GroupID: 2},
  333. {ItemID: 300, GroupID: 3},
  334. },
  335. want: 500,
  336. },
  337. {
  338. name: "handles single conflict and finds next",
  339. randInt: func(n int) int { return 42 },
  340. items: []wire.FeedbagItem{
  341. {ItemID: 42, GroupID: 1},
  342. },
  343. want: 43,
  344. },
  345. {
  346. name: "finds ID before starting point when wrapping",
  347. randInt: func(n int) int { return 5 },
  348. items: []wire.FeedbagItem{
  349. {ItemID: 5, GroupID: 1},
  350. {ItemID: 6, GroupID: 1},
  351. {ItemID: 7, GroupID: 1},
  352. },
  353. want: 8,
  354. },
  355. }
  356. for _, tc := range tt {
  357. t.Run(tc.name, func(t *testing.T) {
  358. fl := NewFeedbagList(tc.items, tc.randInt)
  359. got := fl.genID()
  360. assert.Equal(t, tc.want, got)
  361. })
  362. }
  363. }
  364. func TestFeedbagList_AddGroup(t *testing.T) {
  365. t.Run("creates group and root group when none exists", func(t *testing.T) {
  366. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  367. group := fl.AddGroup("Buddies")
  368. assert.Equal(t, uint16(5), group.GroupID)
  369. assert.Equal(t, "Buddies", group.Name)
  370. assert.Equal(t, wire.FeedbagClassIdGroup, group.ClassID)
  371. upserts := fl.PendingUpdates()
  372. assert.Len(t, upserts, 2)
  373. root := upserts[0]
  374. assert.Equal(t, wire.FeedbagClassIdGroup, root.ClassID)
  375. assert.Equal(t, uint16(0), root.GroupID)
  376. order, ok := root.Uint16SliceBE(wire.FeedbagAttributesOrder)
  377. assert.True(t, ok)
  378. assert.Equal(t, []uint16{5}, order)
  379. })
  380. t.Run("updates existing root group order", func(t *testing.T) {
  381. existing := []wire.FeedbagItem{
  382. {
  383. ClassID: wire.FeedbagClassIdGroup,
  384. GroupID: 0,
  385. TLVLBlock: wire.TLVLBlock{
  386. TLVList: wire.TLVList{
  387. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  388. },
  389. },
  390. },
  391. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  392. }
  393. callCount := 0
  394. fl := NewFeedbagList(existing, func(n int) int {
  395. callCount++
  396. return callCount + 1
  397. })
  398. group := fl.AddGroup("Coworkers")
  399. upserts := fl.PendingUpdates()
  400. assert.Len(t, upserts, 2)
  401. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  402. assert.True(t, ok)
  403. assert.Equal(t, []uint16{1, group.GroupID}, order)
  404. })
  405. t.Run("multiple AddGroup calls accumulate in root order", func(t *testing.T) {
  406. callCount := 0
  407. fl := NewFeedbagList(nil, func(n int) int {
  408. callCount++
  409. return callCount * 10
  410. })
  411. g1 := fl.AddGroup("Group1")
  412. g2 := fl.AddGroup("Group2")
  413. upserts := fl.PendingUpdates()
  414. assert.Len(t, upserts, 3)
  415. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  416. assert.True(t, ok)
  417. assert.Equal(t, []uint16{g1.GroupID, g2.GroupID}, order)
  418. })
  419. }
  420. func TestFeedbagList_DeleteGroup(t *testing.T) {
  421. t.Run("updates root group order", func(t *testing.T) {
  422. fl := NewFeedbagList([]wire.FeedbagItem{
  423. {
  424. Name: "", ClassID: wire.FeedbagClassIdGroup, GroupID: 0,
  425. TLVLBlock: wire.TLVLBlock{
  426. TLVList: wire.TLVList{
  427. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2, 3}),
  428. },
  429. },
  430. },
  431. {
  432. Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1,
  433. TLVLBlock: wire.TLVLBlock{
  434. TLVList: wire.TLVList{
  435. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  436. },
  437. },
  438. },
  439. {Name: "Jane", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 1},
  440. {
  441. Name: "Coworkers", ClassID: wire.FeedbagClassIdGroup, GroupID: 2,
  442. TLVLBlock: wire.TLVLBlock{
  443. TLVList: wire.TLVList{
  444. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2, 3}),
  445. },
  446. },
  447. },
  448. {Name: "Joe", ClassID: wire.FeedbagClassIdBuddy, GroupID: 2, ItemID: 2},
  449. {Name: "Fred", ClassID: wire.FeedbagClassIdBuddy, GroupID: 2, ItemID: 3},
  450. {Name: "Family", ClassID: wire.FeedbagClassIdGroup, GroupID: 3,
  451. TLVLBlock: wire.TLVLBlock{
  452. TLVList: wire.TLVList{
  453. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{4}),
  454. },
  455. },
  456. },
  457. {Name: "Alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 3, ItemID: 4},
  458. }, nil)
  459. fl.DeleteGroup("Coworkers")
  460. deletes := fl.PendingDeletes()
  461. assert.Len(t, deletes, 3)
  462. assert.Equal(t, "Coworkers", deletes[0].Name)
  463. assert.Equal(t, uint16(2), deletes[0].GroupID)
  464. assert.Equal(t, "Joe", deletes[1].Name)
  465. assert.Equal(t, uint16(2), deletes[1].GroupID)
  466. assert.Equal(t, "Fred", deletes[2].Name)
  467. assert.Equal(t, uint16(2), deletes[2].GroupID)
  468. upserts := fl.PendingUpdates()
  469. assert.Len(t, upserts, 1)
  470. assert.Equal(t, uint16(0), upserts[0].GroupID)
  471. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  472. assert.True(t, ok)
  473. assert.Equal(t, []uint16{1, 3}, order)
  474. })
  475. }
  476. func TestFeedbagList_AddBuddy(t *testing.T) {
  477. t.Run("updates parent group order", func(t *testing.T) {
  478. fl := NewFeedbagList([]wire.FeedbagItem{
  479. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  480. }, func(n int) int { return 50 })
  481. inserted, err := fl.AddBuddy("Buddies", "alice", "", "")
  482. assert.NoError(t, err)
  483. assert.True(t, inserted)
  484. upserts := fl.PendingUpdates()
  485. assert.Len(t, upserts, 2)
  486. assert.Equal(t, uint16(1), upserts[1].GroupID)
  487. order, ok := upserts[1].Uint16SliceBE(wire.FeedbagAttributesOrder)
  488. assert.True(t, ok)
  489. assert.Equal(t, []uint16{upserts[0].ItemID}, order)
  490. })
  491. t.Run("multiple buddies accumulate in parent group order", func(t *testing.T) {
  492. fl := NewFeedbagList([]wire.FeedbagItem{
  493. {
  494. Name: "Buddies",
  495. ClassID: wire.FeedbagClassIdGroup,
  496. GroupID: 1,
  497. TLVLBlock: wire.TLVLBlock{
  498. TLVList: wire.TLVList{
  499. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10}),
  500. },
  501. },
  502. },
  503. }, func(n int) int { return 50 })
  504. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  505. assert.NoError(t, err)
  506. _, err = fl.AddBuddy("Buddies", "bob", "", "")
  507. assert.NoError(t, err)
  508. upserts := fl.PendingUpdates()
  509. assert.Len(t, upserts, 3)
  510. order, ok := upserts[1].Uint16SliceBE(wire.FeedbagAttributesOrder)
  511. assert.True(t, ok)
  512. assert.Equal(t, []uint16{10, upserts[0].ItemID, upserts[2].ItemID}, order)
  513. })
  514. t.Run("returns error when parent group does not exist", func(t *testing.T) {
  515. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  516. _, err := fl.AddBuddy("Nonexistent", "alice", "", "")
  517. assert.ErrorContains(t, err, "group \"Nonexistent\" not found")
  518. })
  519. t.Run("normalized screen name: stores buddy with lowercase name", func(t *testing.T) {
  520. fl := NewFeedbagList([]wire.FeedbagItem{
  521. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  522. }, func(n int) int { return 1 })
  523. inserted, err := fl.AddBuddy("Buddies", "Alice", "", "")
  524. assert.NoError(t, err)
  525. assert.True(t, inserted)
  526. upserts := fl.PendingUpdates()
  527. var buddy *wire.FeedbagItem
  528. for i := range upserts {
  529. if upserts[i].ClassID == wire.FeedbagClassIdBuddy {
  530. buddy = &upserts[i]
  531. break
  532. }
  533. }
  534. assert.NotNil(t, buddy)
  535. assert.Equal(t, "alice", buddy.Name)
  536. })
  537. t.Run("normalized screen name: second AddBuddy with different case does not insert duplicate", func(t *testing.T) {
  538. fl := NewFeedbagList([]wire.FeedbagItem{
  539. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  540. }, func(n int) int { return 1 })
  541. inserted1, err := fl.AddBuddy("Buddies", "alice", "", "")
  542. assert.NoError(t, err)
  543. assert.True(t, inserted1)
  544. inserted2, err := fl.AddBuddy("Buddies", "ALICE", "", "")
  545. assert.NoError(t, err)
  546. assert.False(t, inserted2)
  547. })
  548. t.Run("normalized screen name: DeleteBuddy finds buddy by different case", func(t *testing.T) {
  549. fl := NewFeedbagList([]wire.FeedbagItem{
  550. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  551. }, func(n int) int { return 1 })
  552. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  553. assert.NoError(t, err)
  554. _ = fl.PendingUpdates()
  555. err = fl.DeleteBuddy("Buddies", "Alice")
  556. assert.NoError(t, err)
  557. deletes := fl.PendingDeletes()
  558. assert.Len(t, deletes, 1)
  559. assert.Equal(t, "alice", deletes[0].Name)
  560. })
  561. }
  562. func TestFeedbagList_DeleteBuddy(t *testing.T) {
  563. t.Run("updates parent group order", func(t *testing.T) {
  564. fl := NewFeedbagList([]wire.FeedbagItem{
  565. {
  566. Name: "Buddies",
  567. ClassID: wire.FeedbagClassIdGroup,
  568. GroupID: 1,
  569. TLVLBlock: wire.TLVLBlock{
  570. TLVList: wire.TLVList{
  571. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20, 30}),
  572. },
  573. },
  574. },
  575. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 10},
  576. {Name: "bob", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 20},
  577. {Name: "charlie", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 30},
  578. }, nil)
  579. err := fl.DeleteBuddy("Buddies", "bob")
  580. assert.NoError(t, err)
  581. deletes := fl.PendingDeletes()
  582. assert.Len(t, deletes, 1)
  583. assert.Equal(t, "bob", deletes[0].Name)
  584. assert.Equal(t, uint16(20), deletes[0].ItemID)
  585. upserts := fl.PendingUpdates()
  586. assert.Len(t, upserts, 1)
  587. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  588. assert.True(t, ok)
  589. assert.Equal(t, []uint16{10, 30}, order)
  590. })
  591. t.Run("removes only buddy in specified group when same screen name in two groups", func(t *testing.T) {
  592. fl := NewFeedbagList([]wire.FeedbagItem{
  593. {
  594. Name: "Buddies",
  595. ClassID: wire.FeedbagClassIdGroup,
  596. GroupID: 1,
  597. TLVLBlock: wire.TLVLBlock{
  598. TLVList: wire.TLVList{
  599. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20}),
  600. },
  601. },
  602. },
  603. {
  604. Name: "Coworkers",
  605. ClassID: wire.FeedbagClassIdGroup,
  606. GroupID: 2,
  607. TLVLBlock: wire.TLVLBlock{
  608. TLVList: wire.TLVList{
  609. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{20}),
  610. },
  611. },
  612. },
  613. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 2, ItemID: 20},
  614. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 10},
  615. }, nil)
  616. err := fl.DeleteBuddy("Buddies", "alice")
  617. assert.NoError(t, err)
  618. deletes := fl.PendingDeletes()
  619. assert.Len(t, deletes, 1)
  620. assert.Equal(t, "alice", deletes[0].Name)
  621. assert.Equal(t, uint16(1), deletes[0].GroupID, "should delete from Buddies (group 1), not Coworkers (group 2)")
  622. assert.Equal(t, uint16(10), deletes[0].ItemID)
  623. })
  624. }
  625. func TestFeedbagList_PendingUpdates(t *testing.T) {
  626. t.Run("empty when nothing inserted", func(t *testing.T) {
  627. fl := NewFeedbagList(nil, func(n int) int { return 1 })
  628. assert.Nil(t, fl.PendingUpdates())
  629. })
  630. t.Run("includes inserts and updates", func(t *testing.T) {
  631. fl := NewFeedbagList([]wire.FeedbagItem{
  632. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  633. }, func(n int) int { return 50 })
  634. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  635. assert.NoError(t, err)
  636. _, err = fl.AddBuddy("Buddies", "bob", "", "")
  637. assert.NoError(t, err)
  638. upserts := fl.PendingUpdates()
  639. assert.Len(t, upserts, 3)
  640. assert.Equal(t, wire.FeedbagClassIdBuddy, upserts[0].ClassID)
  641. assert.Equal(t, wire.FeedbagClassIdGroup, upserts[1].ClassID)
  642. assert.Equal(t, wire.FeedbagClassIdBuddy, upserts[2].ClassID)
  643. })
  644. t.Run("clears after retrieval", func(t *testing.T) {
  645. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  646. fl.AddGroup("Buddies")
  647. assert.Len(t, fl.PendingUpdates(), 2)
  648. assert.Nil(t, fl.PendingUpdates())
  649. })
  650. }
  651. func TestFeedbagList_PendingDeletes(t *testing.T) {
  652. t.Run("empty when nothing deleted", func(t *testing.T) {
  653. fl := NewFeedbagList(nil, nil)
  654. assert.Nil(t, fl.PendingDeletes())
  655. })
  656. t.Run("clears after retrieval", func(t *testing.T) {
  657. fl := NewFeedbagList([]wire.FeedbagItem{
  658. {Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 1},
  659. }, nil)
  660. fl.deleteItem(wire.FeedbagItem{Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 1})
  661. assert.Len(t, fl.PendingDeletes(), 1)
  662. assert.Nil(t, fl.PendingDeletes())
  663. })
  664. }
  665. func TestFeedbagList_PendingUpdates_upsertsOnly(t *testing.T) {
  666. t.Run("tracks upserted items", func(t *testing.T) {
  667. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  668. result, inserted := fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "alice"})
  669. assert.True(t, inserted)
  670. upserts := fl.PendingUpdates()
  671. assert.Len(t, upserts, 1)
  672. assert.Equal(t, "alice", upserts[0].Name)
  673. assert.Equal(t, uint16(5), result.ItemID)
  674. })
  675. t.Run("clears after retrieval", func(t *testing.T) {
  676. fl := NewFeedbagList(nil, func(n int) int { return 5 })
  677. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit})
  678. assert.Len(t, fl.PendingUpdates(), 1)
  679. assert.Nil(t, fl.PendingUpdates())
  680. })
  681. t.Run("multiple inserts accumulate", func(t *testing.T) {
  682. fl := NewFeedbagList(nil, func(n int) int { return 10 })
  683. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "alice"})
  684. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "bob"})
  685. upserts := fl.PendingUpdates()
  686. assert.Len(t, upserts, 2)
  687. })
  688. }