feedbag_list_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. package toc
  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: "",
  425. ClassID: wire.FeedbagClassIdGroup,
  426. GroupID: 0,
  427. TLVLBlock: wire.TLVLBlock{
  428. TLVList: wire.TLVList{
  429. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2, 3}),
  430. },
  431. },
  432. },
  433. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  434. {Name: "Coworkers", ClassID: wire.FeedbagClassIdGroup, GroupID: 2},
  435. {Name: "Family", ClassID: wire.FeedbagClassIdGroup, GroupID: 3},
  436. }, nil)
  437. fl.DeleteGroup("Coworkers")
  438. deletes := fl.PendingDeletes()
  439. assert.Len(t, deletes, 1)
  440. assert.Equal(t, "Coworkers", deletes[0].Name)
  441. assert.Equal(t, uint16(2), deletes[0].GroupID)
  442. upserts := fl.PendingUpdates()
  443. assert.Len(t, upserts, 1)
  444. assert.Equal(t, uint16(0), upserts[0].GroupID)
  445. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  446. assert.True(t, ok)
  447. assert.Equal(t, []uint16{1, 3}, order)
  448. })
  449. }
  450. func TestFeedbagList_AddBuddy(t *testing.T) {
  451. t.Run("updates parent group order", func(t *testing.T) {
  452. fl := newFeedbagList([]wire.FeedbagItem{
  453. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  454. }, func(n int) int { return 50 })
  455. inserted, err := fl.AddBuddy("Buddies", "alice", "", "")
  456. assert.NoError(t, err)
  457. assert.True(t, inserted)
  458. upserts := fl.PendingUpdates()
  459. assert.Len(t, upserts, 2)
  460. assert.Equal(t, uint16(1), upserts[1].GroupID)
  461. order, ok := upserts[1].Uint16SliceBE(wire.FeedbagAttributesOrder)
  462. assert.True(t, ok)
  463. assert.Equal(t, []uint16{upserts[0].ItemID}, order)
  464. })
  465. t.Run("multiple buddies accumulate in parent group order", func(t *testing.T) {
  466. fl := newFeedbagList([]wire.FeedbagItem{
  467. {
  468. Name: "Buddies",
  469. ClassID: wire.FeedbagClassIdGroup,
  470. GroupID: 1,
  471. TLVLBlock: wire.TLVLBlock{
  472. TLVList: wire.TLVList{
  473. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10}),
  474. },
  475. },
  476. },
  477. }, func(n int) int { return 50 })
  478. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  479. assert.NoError(t, err)
  480. _, err = fl.AddBuddy("Buddies", "bob", "", "")
  481. assert.NoError(t, err)
  482. upserts := fl.PendingUpdates()
  483. assert.Len(t, upserts, 3)
  484. order, ok := upserts[1].Uint16SliceBE(wire.FeedbagAttributesOrder)
  485. assert.True(t, ok)
  486. assert.Equal(t, []uint16{10, upserts[0].ItemID, upserts[2].ItemID}, order)
  487. })
  488. t.Run("returns error when parent group does not exist", func(t *testing.T) {
  489. fl := newFeedbagList(nil, func(n int) int { return 5 })
  490. _, err := fl.AddBuddy("Nonexistent", "alice", "", "")
  491. assert.ErrorContains(t, err, "group \"Nonexistent\" not found")
  492. })
  493. t.Run("normalized screen name: stores buddy with lowercase name", func(t *testing.T) {
  494. fl := newFeedbagList([]wire.FeedbagItem{
  495. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  496. }, func(n int) int { return 1 })
  497. inserted, err := fl.AddBuddy("Buddies", "Alice", "", "")
  498. assert.NoError(t, err)
  499. assert.True(t, inserted)
  500. upserts := fl.PendingUpdates()
  501. var buddy *wire.FeedbagItem
  502. for i := range upserts {
  503. if upserts[i].ClassID == wire.FeedbagClassIdBuddy {
  504. buddy = &upserts[i]
  505. break
  506. }
  507. }
  508. assert.NotNil(t, buddy)
  509. assert.Equal(t, "alice", buddy.Name)
  510. })
  511. t.Run("normalized screen name: second AddBuddy with different case does not insert duplicate", func(t *testing.T) {
  512. fl := newFeedbagList([]wire.FeedbagItem{
  513. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  514. }, func(n int) int { return 1 })
  515. inserted1, err := fl.AddBuddy("Buddies", "alice", "", "")
  516. assert.NoError(t, err)
  517. assert.True(t, inserted1)
  518. inserted2, err := fl.AddBuddy("Buddies", "ALICE", "", "")
  519. assert.NoError(t, err)
  520. assert.False(t, inserted2)
  521. })
  522. t.Run("normalized screen name: DeleteBuddy finds buddy by different case", func(t *testing.T) {
  523. fl := newFeedbagList([]wire.FeedbagItem{
  524. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  525. }, func(n int) int { return 1 })
  526. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  527. assert.NoError(t, err)
  528. _ = fl.PendingUpdates()
  529. err = fl.DeleteBuddy("Buddies", "Alice")
  530. assert.NoError(t, err)
  531. deletes := fl.PendingDeletes()
  532. assert.Len(t, deletes, 1)
  533. assert.Equal(t, "alice", deletes[0].Name)
  534. })
  535. }
  536. func TestFeedbagList_DeleteBuddy(t *testing.T) {
  537. t.Run("updates parent group order", func(t *testing.T) {
  538. fl := newFeedbagList([]wire.FeedbagItem{
  539. {
  540. Name: "Buddies",
  541. ClassID: wire.FeedbagClassIdGroup,
  542. GroupID: 1,
  543. TLVLBlock: wire.TLVLBlock{
  544. TLVList: wire.TLVList{
  545. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20, 30}),
  546. },
  547. },
  548. },
  549. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 10},
  550. {Name: "bob", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 20},
  551. {Name: "charlie", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 30},
  552. }, nil)
  553. err := fl.DeleteBuddy("Buddies", "bob")
  554. assert.NoError(t, err)
  555. deletes := fl.PendingDeletes()
  556. assert.Len(t, deletes, 1)
  557. assert.Equal(t, "bob", deletes[0].Name)
  558. assert.Equal(t, uint16(20), deletes[0].ItemID)
  559. upserts := fl.PendingUpdates()
  560. assert.Len(t, upserts, 1)
  561. order, ok := upserts[0].Uint16SliceBE(wire.FeedbagAttributesOrder)
  562. assert.True(t, ok)
  563. assert.Equal(t, []uint16{10, 30}, order)
  564. })
  565. t.Run("removes only buddy in specified group when same screen name in two groups", func(t *testing.T) {
  566. fl := newFeedbagList([]wire.FeedbagItem{
  567. {
  568. Name: "Buddies",
  569. ClassID: wire.FeedbagClassIdGroup,
  570. GroupID: 1,
  571. TLVLBlock: wire.TLVLBlock{
  572. TLVList: wire.TLVList{
  573. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20}),
  574. },
  575. },
  576. },
  577. {
  578. Name: "Coworkers",
  579. ClassID: wire.FeedbagClassIdGroup,
  580. GroupID: 2,
  581. TLVLBlock: wire.TLVLBlock{
  582. TLVList: wire.TLVList{
  583. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{20}),
  584. },
  585. },
  586. },
  587. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 2, ItemID: 20},
  588. {Name: "alice", ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, ItemID: 10},
  589. }, nil)
  590. err := fl.DeleteBuddy("Buddies", "alice")
  591. assert.NoError(t, err)
  592. deletes := fl.PendingDeletes()
  593. assert.Len(t, deletes, 1)
  594. assert.Equal(t, "alice", deletes[0].Name)
  595. assert.Equal(t, uint16(1), deletes[0].GroupID, "should delete from Buddies (group 1), not Coworkers (group 2)")
  596. assert.Equal(t, uint16(10), deletes[0].ItemID)
  597. })
  598. }
  599. func TestFeedbagList_PendingUpdates(t *testing.T) {
  600. t.Run("empty when nothing inserted", func(t *testing.T) {
  601. fl := newFeedbagList(nil, func(n int) int { return 1 })
  602. assert.Nil(t, fl.PendingUpdates())
  603. })
  604. t.Run("includes inserts and updates", func(t *testing.T) {
  605. fl := newFeedbagList([]wire.FeedbagItem{
  606. {Name: "Buddies", ClassID: wire.FeedbagClassIdGroup, GroupID: 1},
  607. }, func(n int) int { return 50 })
  608. _, err := fl.AddBuddy("Buddies", "alice", "", "")
  609. assert.NoError(t, err)
  610. _, err = fl.AddBuddy("Buddies", "bob", "", "")
  611. assert.NoError(t, err)
  612. upserts := fl.PendingUpdates()
  613. assert.Len(t, upserts, 3)
  614. assert.Equal(t, wire.FeedbagClassIdBuddy, upserts[0].ClassID)
  615. assert.Equal(t, wire.FeedbagClassIdGroup, upserts[1].ClassID)
  616. assert.Equal(t, wire.FeedbagClassIdBuddy, upserts[2].ClassID)
  617. })
  618. t.Run("clears after retrieval", func(t *testing.T) {
  619. fl := newFeedbagList(nil, func(n int) int { return 5 })
  620. fl.AddGroup("Buddies")
  621. assert.Len(t, fl.PendingUpdates(), 2)
  622. assert.Nil(t, fl.PendingUpdates())
  623. })
  624. }
  625. func TestFeedbagList_PendingDeletes(t *testing.T) {
  626. t.Run("empty when nothing deleted", func(t *testing.T) {
  627. fl := newFeedbagList(nil, nil)
  628. assert.Nil(t, fl.PendingDeletes())
  629. })
  630. t.Run("clears after retrieval", func(t *testing.T) {
  631. fl := newFeedbagList([]wire.FeedbagItem{
  632. {Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 1},
  633. }, nil)
  634. fl.deleteItem(wire.FeedbagItem{Name: "alice", ClassID: wire.FeedbagClassIDPermit, ItemID: 1})
  635. assert.Len(t, fl.PendingDeletes(), 1)
  636. assert.Nil(t, fl.PendingDeletes())
  637. })
  638. }
  639. func TestFeedbagList_PendingUpdates_upsertsOnly(t *testing.T) {
  640. t.Run("tracks upserted items", func(t *testing.T) {
  641. fl := newFeedbagList(nil, func(n int) int { return 5 })
  642. result, inserted := fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "alice"})
  643. assert.True(t, inserted)
  644. upserts := fl.PendingUpdates()
  645. assert.Len(t, upserts, 1)
  646. assert.Equal(t, "alice", upserts[0].Name)
  647. assert.Equal(t, uint16(5), result.ItemID)
  648. })
  649. t.Run("clears after retrieval", func(t *testing.T) {
  650. fl := newFeedbagList(nil, func(n int) int { return 5 })
  651. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit})
  652. assert.Len(t, fl.PendingUpdates(), 1)
  653. assert.Nil(t, fl.PendingUpdates())
  654. })
  655. t.Run("multiple inserts accumulate", func(t *testing.T) {
  656. fl := newFeedbagList(nil, func(n int) int { return 10 })
  657. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "alice"})
  658. fl.upsertItem(wire.FeedbagItem{ClassID: wire.FeedbagClassIDPermit, Name: "bob"})
  659. upserts := fl.PendingUpdates()
  660. assert.Len(t, upserts, 2)
  661. })
  662. }