rest_test.go 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010
  1. // Copyright 2020 gorse Project Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package server
  15. import (
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "net/http"
  20. "strconv"
  21. "testing"
  22. "time"
  23. "github.com/emicklei/go-restful/v3"
  24. "github.com/samber/lo"
  25. "github.com/steinfletcher/apitest"
  26. "github.com/stretchr/testify/assert"
  27. "github.com/stretchr/testify/suite"
  28. "github.com/zhenghaoz/gorse/config"
  29. "github.com/zhenghaoz/gorse/storage/cache"
  30. "github.com/zhenghaoz/gorse/storage/data"
  31. "google.golang.org/protobuf/proto"
  32. )
  33. const apiKey = "test_api_key"
  34. type ServerTestSuite struct {
  35. suite.Suite
  36. RestServer
  37. handler *restful.Container
  38. }
  39. func (suite *ServerTestSuite) SetupSuite() {
  40. // create mock redis server
  41. var err error
  42. // open database
  43. suite.Settings = config.NewSettings()
  44. suite.DataClient, err = data.Open(fmt.Sprintf("sqlite://%s/data.db", suite.T().TempDir()), "")
  45. suite.NoError(err)
  46. suite.CacheClient, err = cache.Open(fmt.Sprintf("sqlite://%s/cache.db", suite.T().TempDir()), "")
  47. suite.NoError(err)
  48. // init database
  49. err = suite.DataClient.Init()
  50. suite.NoError(err)
  51. err = suite.CacheClient.Init()
  52. suite.NoError(err)
  53. suite.WebService = new(restful.WebService)
  54. suite.CreateWebService()
  55. // create handler
  56. suite.handler = restful.NewContainer()
  57. suite.handler.Add(suite.WebService)
  58. }
  59. func (suite *ServerTestSuite) TearDownSuite() {
  60. err := suite.DataClient.Close()
  61. suite.NoError(err)
  62. err = suite.CacheClient.Close()
  63. suite.NoError(err)
  64. }
  65. func (suite *ServerTestSuite) SetupTest() {
  66. err := suite.DataClient.Purge()
  67. suite.NoError(err)
  68. err = suite.CacheClient.Purge()
  69. suite.NoError(err)
  70. // configuration
  71. suite.Config = config.GetDefaultConfig()
  72. suite.Config.Server.APIKey = apiKey
  73. }
  74. func (suite *ServerTestSuite) marshal(v interface{}) string {
  75. s, err := json.Marshal(v)
  76. suite.NoError(err)
  77. return string(s)
  78. }
  79. func (suite *ServerTestSuite) TestUsers() {
  80. t := suite.T()
  81. users := []data.User{
  82. {UserId: "0"},
  83. {UserId: "1"},
  84. {UserId: "2"},
  85. {UserId: "3"},
  86. {UserId: "4"},
  87. }
  88. apitest.New().
  89. Handler(suite.handler).
  90. Post("/api/user").
  91. Header("X-API-Key", apiKey).
  92. JSON(users[0]).
  93. Expect(t).
  94. Status(http.StatusOK).
  95. Body(`{"RowAffected":1}`).
  96. End()
  97. apitest.New().
  98. Handler(suite.handler).
  99. Get("/api/user/0").
  100. Header("X-API-Key", apiKey).
  101. Expect(t).
  102. Status(http.StatusOK).
  103. Body(suite.marshal(users[0])).
  104. End()
  105. apitest.New().
  106. Handler(suite.handler).
  107. Post("/api/users").
  108. Header("X-API-Key", apiKey).
  109. JSON(users[1:]).
  110. Expect(t).
  111. Status(http.StatusOK).
  112. Body(`{"RowAffected":4}`).
  113. End()
  114. apitest.New().
  115. Handler(suite.handler).
  116. Get("/api/users").
  117. Header("X-API-Key", apiKey).
  118. QueryParams(map[string]string{
  119. "cursor": "",
  120. "n": "100",
  121. }).
  122. Expect(t).
  123. Status(http.StatusOK).
  124. Body(suite.marshal(UserIterator{
  125. Cursor: "",
  126. Users: users,
  127. })).
  128. End()
  129. apitest.New().
  130. Handler(suite.handler).
  131. Delete("/api/user/0").
  132. Header("X-API-Key", apiKey).
  133. Expect(t).
  134. Status(http.StatusOK).
  135. Body(`{"RowAffected": 1}`).
  136. End()
  137. apitest.New().
  138. Handler(suite.handler).
  139. Get("/api/user/0").
  140. Header("X-API-Key", apiKey).
  141. Expect(t).
  142. Status(http.StatusNotFound).
  143. End()
  144. // test modify
  145. apitest.New().
  146. Handler(suite.handler).
  147. Patch("/api/user/1").
  148. Header("X-API-Key", apiKey).
  149. JSON(data.UserPatch{Labels: []string{"a", "b", "c"}, Comment: proto.String("modified")}).
  150. Expect(t).
  151. Status(http.StatusOK).
  152. Body(`{"RowAffected": 1}`).
  153. End()
  154. apitest.New().
  155. Handler(suite.handler).
  156. Get("/api/user/1").
  157. Header("X-API-Key", apiKey).
  158. Expect(t).
  159. Status(http.StatusOK).
  160. Body(suite.marshal(data.User{
  161. UserId: "1",
  162. Comment: "modified",
  163. Labels: []string{"a", "b", "c"},
  164. })).
  165. End()
  166. // malicious labels
  167. apitest.New().
  168. Handler(suite.handler).
  169. Post("/api/user").
  170. Header("X-API-Key", apiKey).
  171. JSON(data.User{UserId: "malicious", Labels: []any{"price", 100}}).
  172. Expect(t).
  173. Status(http.StatusBadRequest).
  174. End()
  175. apitest.New().
  176. Handler(suite.handler).
  177. Post("/api/users").
  178. Header("X-API-Key", apiKey).
  179. JSON([]data.User{{UserId: "malicious", Labels: []any{"price", 100}}}).
  180. Expect(t).
  181. Status(http.StatusBadRequest).
  182. End()
  183. apitest.New().
  184. Handler(suite.handler).
  185. Patch("/api/user/malicious").
  186. Header("X-API-Key", apiKey).
  187. JSON(data.UserPatch{Labels: []any{"price", 100}}).
  188. Expect(t).
  189. Status(http.StatusBadRequest).
  190. End()
  191. }
  192. func (suite *ServerTestSuite) TestItems() {
  193. ctx := context.Background()
  194. t := suite.T()
  195. // Items
  196. items := []data.Item{
  197. {
  198. ItemId: "0",
  199. IsHidden: true,
  200. Timestamp: time.Date(1996, 3, 15, 0, 0, 0, 0, time.UTC),
  201. Labels: []string{"a"},
  202. Comment: "comment_0",
  203. },
  204. {
  205. ItemId: "2",
  206. Categories: []string{"*"},
  207. Timestamp: time.Date(1997, 3, 15, 0, 0, 0, 0, time.UTC),
  208. Labels: []string{"a"},
  209. Comment: "comment_2",
  210. },
  211. {
  212. ItemId: "4",
  213. IsHidden: true,
  214. Timestamp: time.Date(1998, 3, 15, 0, 0, 0, 0, time.UTC),
  215. Labels: []string{"a", "b"},
  216. Comment: "comment_4",
  217. },
  218. {
  219. ItemId: "6",
  220. Categories: []string{"*"},
  221. Timestamp: time.Date(1999, 3, 15, 0, 0, 0, 0, time.UTC),
  222. Labels: []string{"b"},
  223. Comment: "comment_6",
  224. },
  225. {
  226. ItemId: "8",
  227. IsHidden: true,
  228. Timestamp: time.Date(2000, 3, 15, 0, 0, 0, 0, time.UTC),
  229. Labels: []string{"b"},
  230. Comment: "comment_8",
  231. },
  232. }
  233. // insert popular scores
  234. err := suite.CacheClient.AddDocuments(ctx, cache.PopularItems, "", []cache.Document{
  235. {Id: "0", Score: 10},
  236. {Id: "2", Score: 12},
  237. {Id: "4", Score: 14},
  238. {Id: "6", Score: 16},
  239. {Id: "8", Score: 18},
  240. })
  241. assert.NoError(t, err)
  242. // insert items
  243. apitest.New().
  244. Handler(suite.handler).
  245. Post("/api/item").
  246. Header("X-API-Key", apiKey).
  247. JSON(items[0]).
  248. Expect(t).
  249. Status(http.StatusOK).
  250. Body(`{"RowAffected": 1}`).
  251. End()
  252. // batch insert items
  253. apitest.New().
  254. Handler(suite.handler).
  255. Post("/api/items").
  256. Header("X-API-Key", apiKey).
  257. JSON(items[1:]).
  258. Expect(t).
  259. Status(http.StatusOK).
  260. Body(`{"RowAffected": 4}`).
  261. End()
  262. // get items
  263. apitest.New().
  264. Handler(suite.handler).
  265. Get("/api/items").
  266. Header("X-API-Key", apiKey).
  267. QueryParams(map[string]string{
  268. "cursor": "",
  269. "n": "100",
  270. }).
  271. Expect(t).
  272. Status(http.StatusOK).
  273. Body(suite.marshal(ItemIterator{
  274. Cursor: "",
  275. Items: items,
  276. })).
  277. End()
  278. // get latest items
  279. apitest.New().
  280. Handler(suite.handler).
  281. Get("/api/latest").
  282. Header("X-API-Key", apiKey).
  283. QueryParams(map[string]string{
  284. "n": "3",
  285. }).
  286. Expect(t).
  287. Status(http.StatusOK).
  288. Body(suite.marshal([]cache.Document{
  289. {Id: items[3].ItemId, Score: float64(items[3].Timestamp.Unix())},
  290. {Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},
  291. })).
  292. End()
  293. apitest.New().
  294. Handler(suite.handler).
  295. Get("/api/latest/*").
  296. Header("X-API-Key", apiKey).
  297. QueryParams(map[string]string{
  298. "n": "3",
  299. }).
  300. Expect(t).
  301. Status(http.StatusOK).
  302. Body(suite.marshal([]cache.Document{
  303. {Id: items[3].ItemId, Score: float64(items[3].Timestamp.Unix())},
  304. {Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},
  305. })).
  306. End()
  307. // get popular items
  308. apitest.New().
  309. Handler(suite.handler).
  310. Get("/api/popular").
  311. Header("X-API-Key", apiKey).
  312. QueryParams(map[string]string{
  313. "n": "3",
  314. }).
  315. Expect(t).
  316. Status(http.StatusOK).
  317. Body(suite.marshal([]cache.Document{
  318. {Id: items[3].ItemId, Score: 16},
  319. {Id: items[1].ItemId, Score: 12},
  320. })).
  321. End()
  322. apitest.New().
  323. Handler(suite.handler).
  324. Get("/api/popular/*").
  325. Header("X-API-Key", apiKey).
  326. QueryParams(map[string]string{
  327. "n": "3",
  328. }).
  329. Expect(t).
  330. Status(http.StatusOK).
  331. Body(suite.marshal([]cache.Document{
  332. {Id: items[3].ItemId, Score: 16},
  333. {Id: items[1].ItemId, Score: 12},
  334. })).
  335. End()
  336. // get categories
  337. categories, err := suite.CacheClient.GetSet(ctx, cache.ItemCategories)
  338. assert.NoError(t, err)
  339. assert.Equal(t, []string{"*"}, categories)
  340. // delete item
  341. apitest.New().
  342. Handler(suite.handler).
  343. Delete("/api/item/6").
  344. Header("X-API-Key", apiKey).
  345. Expect(t).
  346. Status(http.StatusOK).
  347. Body(`{"RowAffected": 1}`).
  348. End()
  349. // get item
  350. apitest.New().
  351. Handler(suite.handler).
  352. Get("/api/item/6").
  353. Header("X-API-Key", apiKey).
  354. Expect(t).
  355. Status(http.StatusNotFound).
  356. End()
  357. // get latest items
  358. apitest.New().
  359. Handler(suite.handler).
  360. Get("/api/latest").
  361. Header("X-API-Key", apiKey).
  362. QueryParams(map[string]string{
  363. "n": "3",
  364. }).
  365. Expect(t).
  366. Status(http.StatusOK).
  367. Body(suite.marshal([]cache.Document{
  368. {Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},
  369. })).
  370. End()
  371. apitest.New().
  372. Handler(suite.handler).
  373. Get("/api/latest/*").
  374. Header("X-API-Key", apiKey).
  375. QueryParams(map[string]string{
  376. "n": "3",
  377. }).
  378. Expect(t).
  379. Status(http.StatusOK).
  380. Body(suite.marshal([]cache.Document{
  381. {Id: items[1].ItemId, Score: float64(items[1].Timestamp.Unix())},
  382. })).
  383. End()
  384. // get popular items
  385. apitest.New().
  386. Handler(suite.handler).
  387. Get("/api/popular").
  388. Header("X-API-Key", apiKey).
  389. QueryParams(map[string]string{
  390. "n": "3",
  391. }).
  392. Expect(t).
  393. Status(http.StatusOK).
  394. Body(suite.marshal([]cache.Document{
  395. {Id: items[1].ItemId, Score: 12},
  396. })).
  397. End()
  398. apitest.New().
  399. Handler(suite.handler).
  400. Get("/api/popular/*").
  401. Header("X-API-Key", apiKey).
  402. QueryParams(map[string]string{
  403. "n": "3",
  404. }).
  405. Expect(t).
  406. Status(http.StatusOK).
  407. Body(suite.marshal([]cache.Document{
  408. {Id: items[1].ItemId, Score: 12},
  409. })).
  410. End()
  411. // test modify
  412. timestamp := time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC)
  413. apitest.New().
  414. Handler(suite.handler).
  415. Patch("/api/item/2").
  416. Header("X-API-Key", apiKey).
  417. JSON(data.ItemPatch{
  418. IsHidden: proto.Bool(true),
  419. Categories: []string{"-"},
  420. Labels: []string{"a", "b", "c"},
  421. Comment: proto.String("modified"),
  422. Timestamp: &timestamp,
  423. }).
  424. Expect(t).
  425. Status(http.StatusOK).
  426. Body(`{"RowAffected": 1}`).
  427. End()
  428. apitest.New().
  429. Handler(suite.handler).
  430. Get("/api/item/2").
  431. Header("X-API-Key", apiKey).
  432. Expect(t).
  433. Status(http.StatusOK).
  434. Body(suite.marshal(data.Item{
  435. ItemId: "2",
  436. IsHidden: true,
  437. Categories: []string{"-"},
  438. Comment: "modified",
  439. Labels: []string{"a", "b", "c"},
  440. Timestamp: timestamp,
  441. })).
  442. End()
  443. apitest.New().
  444. Handler(suite.handler).
  445. Patch("/api/item/2").
  446. Header("X-API-Key", apiKey).
  447. JSON(data.ItemPatch{
  448. IsHidden: proto.Bool(false),
  449. }).
  450. Expect(t).
  451. Status(http.StatusOK).
  452. Body(`{"RowAffected": 1}`).
  453. End()
  454. // get latest items
  455. apitest.New().
  456. Handler(suite.handler).
  457. Get("/api/latest/-").
  458. Header("X-API-Key", apiKey).
  459. QueryParams(map[string]string{
  460. "n": "1",
  461. }).
  462. Expect(t).
  463. Status(http.StatusOK).
  464. Body(suite.marshal([]cache.Document{
  465. {Id: "2", Score: float64(timestamp.Unix())},
  466. })).
  467. End()
  468. apitest.New().
  469. Handler(suite.handler).
  470. Get("/api/latest/*").
  471. Header("X-API-Key", apiKey).
  472. QueryParams(map[string]string{
  473. "n": "3",
  474. }).
  475. Expect(t).
  476. Status(http.StatusOK).
  477. Body(suite.marshal([]cache.Document{})).
  478. End()
  479. // get popular items
  480. apitest.New().
  481. Handler(suite.handler).
  482. Get("/api/popular/-").
  483. Header("X-API-Key", apiKey).
  484. QueryParams(map[string]string{
  485. "n": "1",
  486. }).
  487. Expect(t).
  488. Status(http.StatusOK).
  489. Body(suite.marshal([]cache.Document{
  490. {Id: "2", Score: 12},
  491. })).
  492. End()
  493. apitest.New().
  494. Handler(suite.handler).
  495. Get("/api/popular/*").
  496. Header("X-API-Key", apiKey).
  497. QueryParams(map[string]string{
  498. "n": "3",
  499. }).
  500. Expect(t).
  501. Status(http.StatusOK).
  502. Body(suite.marshal([]cache.Document{})).
  503. End()
  504. // insert category
  505. apitest.New().
  506. Handler(suite.handler).
  507. Put("/api/item/2/category/@").
  508. Header("X-API-Key", apiKey).
  509. Expect(t).
  510. Status(http.StatusOK).
  511. Body(suite.marshal(Success{RowAffected: 1})).
  512. End()
  513. apitest.New().
  514. Handler(suite.handler).
  515. Get("/api/item/2").
  516. Header("X-API-Key", apiKey).
  517. Expect(t).
  518. Status(http.StatusOK).
  519. Body(suite.marshal(data.Item{
  520. ItemId: "2",
  521. IsHidden: false,
  522. Categories: []string{"-", "@"},
  523. Comment: "modified",
  524. Labels: []string{"a", "b", "c"},
  525. Timestamp: timestamp,
  526. })).
  527. End()
  528. // get latest items
  529. apitest.New().
  530. Handler(suite.handler).
  531. Get("/api/latest/@").
  532. Header("X-API-Key", apiKey).
  533. QueryParams(map[string]string{
  534. "n": "1",
  535. }).
  536. Expect(t).
  537. Status(http.StatusOK).
  538. Body(suite.marshal([]cache.Document{
  539. {Id: "2", Score: float64(timestamp.Unix())},
  540. })).
  541. End()
  542. // get popular items
  543. apitest.New().
  544. Handler(suite.handler).
  545. Get("/api/popular/@").
  546. Header("X-API-Key", apiKey).
  547. QueryParams(map[string]string{
  548. "n": "1",
  549. }).
  550. Expect(t).
  551. Status(http.StatusOK).
  552. Body(suite.marshal([]cache.Document{
  553. {Id: "2", Score: 12},
  554. })).
  555. End()
  556. // delete category
  557. apitest.New().
  558. Handler(suite.handler).
  559. Delete("/api/item/2/category/@").
  560. Header("X-API-Key", apiKey).
  561. Expect(t).
  562. Status(http.StatusOK).
  563. Body(suite.marshal(Success{RowAffected: 1})).
  564. End()
  565. apitest.New().
  566. Handler(suite.handler).
  567. Get("/api/item/2").
  568. Header("X-API-Key", apiKey).
  569. Expect(t).
  570. Status(http.StatusOK).
  571. Body(suite.marshal(data.Item{
  572. ItemId: "2",
  573. IsHidden: false,
  574. Categories: []string{"-"},
  575. Comment: "modified",
  576. Labels: []string{"a", "b", "c"},
  577. Timestamp: timestamp,
  578. })).
  579. End()
  580. // get latest items
  581. apitest.New().
  582. Handler(suite.handler).
  583. Get("/api/latest/@").
  584. Header("X-API-Key", apiKey).
  585. QueryParams(map[string]string{
  586. "n": "1",
  587. }).
  588. Expect(t).
  589. Status(http.StatusOK).
  590. Body(suite.marshal([]cache.Document{})).
  591. End()
  592. // get popular items
  593. apitest.New().
  594. Handler(suite.handler).
  595. Get("/api/popular/@").
  596. Header("X-API-Key", apiKey).
  597. QueryParams(map[string]string{
  598. "n": "1",
  599. }).
  600. Expect(t).
  601. Status(http.StatusOK).
  602. Body(suite.marshal([]cache.Document{})).
  603. End()
  604. // insert items without timestamp
  605. apitest.New().
  606. Handler(suite.handler).
  607. Post("/api/item").
  608. Header("X-API-Key", apiKey).
  609. JSON(Item{ItemId: "256"}).
  610. Expect(t).
  611. Status(http.StatusOK).
  612. Body(`{"RowAffected": 1}`).
  613. End()
  614. // malicious labels
  615. apitest.New().
  616. Handler(suite.handler).
  617. Post("/api/item").
  618. Header("X-API-Key", apiKey).
  619. JSON(Item{ItemId: "malicious", Labels: []any{"price", 1}}).
  620. Expect(t).
  621. Status(http.StatusBadRequest).
  622. End()
  623. apitest.New().
  624. Handler(suite.handler).
  625. Post("/api/items").
  626. Header("X-API-Key", apiKey).
  627. JSON([]Item{{ItemId: "malicious", Labels: []any{"price", 1}}}).
  628. Expect(t).
  629. Status(http.StatusBadRequest).
  630. End()
  631. apitest.New().
  632. Handler(suite.handler).
  633. Patch("/api/item/malicious").
  634. Header("X-API-Key", apiKey).
  635. JSON(data.ItemPatch{Labels: []any{"price", 1}}).
  636. Expect(t).
  637. Status(http.StatusBadRequest).
  638. End()
  639. }
  640. func (suite *ServerTestSuite) TestFeedback() {
  641. ctx := context.Background()
  642. t := suite.T()
  643. // Insert ret
  644. feedback := []data.Feedback{
  645. {FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "0", ItemId: "0"}},
  646. {FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "1", ItemId: "2"}},
  647. {FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "2", ItemId: "4"}},
  648. {FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "3", ItemId: "6"}},
  649. {FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "4", ItemId: "8"}},
  650. }
  651. //BatchInsertFeedback
  652. apitest.New().
  653. Handler(suite.handler).
  654. Post("/api/feedback").
  655. Header("X-API-Key", apiKey).
  656. JSON(feedback).
  657. Expect(t).
  658. Status(http.StatusOK).
  659. Body(`{"RowAffected": 5}`).
  660. End()
  661. //Get Feedback
  662. apitest.New().
  663. Handler(suite.handler).
  664. Get("/api/feedback").
  665. Header("X-API-Key", apiKey).
  666. QueryParams(map[string]string{
  667. "cursor": "",
  668. "n": "100",
  669. }).
  670. Expect(t).
  671. Body(suite.marshal(FeedbackIterator{
  672. Cursor: "",
  673. Feedback: feedback,
  674. })).
  675. Status(http.StatusOK).
  676. End()
  677. // get feedback by user
  678. apitest.New().
  679. Handler(suite.handler).
  680. Get("/api/user/1/feedback").
  681. Header("X-API-Key", apiKey).
  682. Expect(t).
  683. Body(suite.marshal([]data.Feedback{feedback[1]})).
  684. Status(http.StatusOK).
  685. End()
  686. // get feedback by item
  687. apitest.New().
  688. Handler(suite.handler).
  689. Get("/api/item/2/feedback").
  690. Header("X-API-Key", apiKey).
  691. Expect(t).
  692. Body(suite.marshal([]data.Feedback{feedback[1]})).
  693. Status(http.StatusOK).
  694. End()
  695. //Get Items
  696. apitest.New().
  697. Handler(suite.handler).
  698. Get("/api/items").
  699. Header("X-API-Key", apiKey).
  700. Expect(t).
  701. Status(http.StatusOK).
  702. Body(suite.marshal(ItemIterator{
  703. Cursor: "",
  704. Items: []data.Item{
  705. {ItemId: "0"},
  706. {ItemId: "2"},
  707. {ItemId: "4"},
  708. {ItemId: "6"},
  709. {ItemId: "8"},
  710. },
  711. })).
  712. End()
  713. apitest.New().
  714. Handler(suite.handler).
  715. Get("/api/users").
  716. Header("X-API-Key", apiKey).
  717. Expect(t).
  718. Status(http.StatusOK).
  719. Body(suite.marshal(UserIterator{
  720. Cursor: "",
  721. Users: []data.User{
  722. {UserId: "0"},
  723. {UserId: "1"},
  724. {UserId: "2"},
  725. {UserId: "3"},
  726. {UserId: "4"}},
  727. })).
  728. End()
  729. apitest.New().
  730. Handler(suite.handler).
  731. Get("/api/user/2/feedback/click").
  732. Header("X-API-Key", apiKey).
  733. Expect(t).
  734. Status(http.StatusOK).
  735. Body(`[{"FeedbackType":"click", "UserId": "2", "ItemId": "4", "Timestamp":"0001-01-01T00:00:00Z","Comment":""}]`).
  736. End()
  737. apitest.New().
  738. Handler(suite.handler).
  739. Get("/api/item/4/feedback/click").
  740. Header("X-API-Key", apiKey).
  741. Expect(t).
  742. Status(http.StatusOK).
  743. Body(`[{"FeedbackType":"click", "UserId": "2", "ItemId": "4", "Timestamp":"0001-01-01T00:00:00Z","Comment":""}]`).
  744. End()
  745. // test overwrite
  746. apitest.New().
  747. Handler(suite.handler).
  748. Put("/api/feedback").
  749. Header("X-API-Key", apiKey).
  750. JSON([]data.Feedback{{
  751. FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "0", ItemId: "0"},
  752. Comment: "override",
  753. }}).
  754. Expect(t).
  755. Status(http.StatusOK).
  756. Body(`{"RowAffected": 1}`).
  757. End()
  758. ret, err := suite.DataClient.GetUserFeedback(ctx, "0", suite.Config.Now(), "click")
  759. assert.NoError(t, err)
  760. assert.Equal(t, 1, len(ret))
  761. assert.Equal(t, "override", ret[0].Comment)
  762. // test not overwrite
  763. apitest.New().
  764. Handler(suite.handler).
  765. Post("/api/feedback").
  766. Header("X-API-Key", apiKey).
  767. JSON([]data.Feedback{{
  768. FeedbackKey: data.FeedbackKey{FeedbackType: "click", UserId: "0", ItemId: "0"},
  769. Comment: "not_override",
  770. }}).
  771. Expect(t).
  772. Status(http.StatusOK).
  773. Body(`{"RowAffected": 1}`).
  774. End()
  775. ret, err = suite.DataClient.GetUserFeedback(ctx, "0", suite.Config.Now(), "click")
  776. assert.NoError(t, err)
  777. assert.Equal(t, 1, len(ret))
  778. assert.Equal(t, "override", ret[0].Comment)
  779. // insert feedback without timestamp
  780. apitest.New().
  781. Handler(suite.handler).
  782. Post("/api/feedback").
  783. Header("X-API-Key", apiKey).
  784. JSON([]Feedback{{FeedbackKey: data.FeedbackKey{UserId: "100", ItemId: "100", FeedbackType: "Type"}}}).
  785. Expect(t).
  786. Status(http.StatusOK).
  787. Body(`{"RowAffected": 1}`).
  788. End()
  789. }
  790. func (suite *ServerTestSuite) TestNonPersonalizedRecommend() {
  791. ctx := context.Background()
  792. type ListOperator struct {
  793. Name string
  794. Collection string
  795. Subset string
  796. Category string
  797. URL string
  798. }
  799. operators := []ListOperator{
  800. // TODO: Support hide users in the future.
  801. //{"User Neighbors", cache.Collection(cache.UserNeighbors, "0"), "/api/user/0/neighbors"},
  802. {"Item Neighbors", cache.ItemNeighbors, "0", "", "/api/item/0/neighbors"},
  803. {"Item Neighbors in Category", cache.ItemNeighbors, "0", "0", "/api/item/0/neighbors/0"},
  804. {"Latest Items", cache.LatestItems, "", "", "/api/latest/"},
  805. {"Latest Items in Category", cache.LatestItems, "", "0", "/api/latest/0"},
  806. {"Popular Items", cache.PopularItems, "", "", "/api/popular/"},
  807. {"Popular Items in Category", cache.PopularItems, "", "0", "/api/popular/0"},
  808. {"Offline Recommend", cache.OfflineRecommend, "0", "", "/api/intermediate/recommend/0"},
  809. {"Offline Recommend in Category", cache.OfflineRecommend, "0", "0", "/api/intermediate/recommend/0/0"},
  810. }
  811. for i, operator := range operators {
  812. suite.T().Run(operator.Name, func(t *testing.T) {
  813. // insert documents
  814. documents := []cache.Document{
  815. {Id: strconv.Itoa(i) + "0", Score: 100, Categories: []string{operator.Category}},
  816. {Id: strconv.Itoa(i) + "1", Score: 99, Categories: []string{operator.Category}},
  817. {Id: strconv.Itoa(i) + "2", Score: 98, Categories: []string{operator.Category}},
  818. {Id: strconv.Itoa(i) + "3", Score: 97, Categories: []string{operator.Category}},
  819. {Id: strconv.Itoa(i) + "4", Score: 96, Categories: []string{operator.Category}},
  820. }
  821. err := suite.CacheClient.AddDocuments(ctx, operator.Collection, operator.Subset, documents)
  822. assert.NoError(t, err)
  823. // hidden item
  824. apitest.New().
  825. Handler(suite.handler).
  826. Patch("/api/item/"+strconv.Itoa(i)+"3").
  827. Header("X-API-Key", apiKey).
  828. JSON(data.ItemPatch{IsHidden: proto.Bool(true)}).
  829. Expect(t).
  830. Status(http.StatusOK).
  831. End()
  832. // insert read feedback
  833. err = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{{
  834. FeedbackKey: data.FeedbackKey{
  835. FeedbackType: "read",
  836. UserId: "0",
  837. ItemId: strconv.Itoa(i) + "1",
  838. },
  839. Timestamp: time.Now().Add(-time.Hour),
  840. }}, true, true, true)
  841. assert.NoError(t, err)
  842. apitest.New().
  843. Handler(suite.handler).
  844. Get(operator.URL).
  845. Header("X-API-Key", apiKey).
  846. Expect(t).
  847. Status(http.StatusOK).
  848. Body(suite.marshal(([]cache.Document{documents[0], documents[1], documents[2], documents[4]}))).
  849. End()
  850. apitest.New().
  851. Handler(suite.handler).
  852. Get(operator.URL).
  853. Header("X-API-Key", apiKey).
  854. QueryParams(map[string]string{
  855. "offset": "0",
  856. "n": "3"}).
  857. Expect(t).
  858. Status(http.StatusOK).
  859. Body(suite.marshal(([]cache.Document{documents[0], documents[1], documents[2]}))).
  860. End()
  861. apitest.New().
  862. Handler(suite.handler).
  863. Get(operator.URL).
  864. Header("X-API-Key", apiKey).
  865. QueryParams(map[string]string{
  866. "offset": "1",
  867. "n": "3"}).
  868. Expect(t).
  869. Status(http.StatusOK).
  870. Body(suite.marshal(([]cache.Document{documents[1], documents[2], documents[4]}))).
  871. End()
  872. apitest.New().
  873. Handler(suite.handler).
  874. Get(operator.URL).
  875. Header("X-API-Key", apiKey).
  876. QueryParams(map[string]string{
  877. "offset": "0",
  878. "n": "0"}).
  879. Expect(t).
  880. Status(http.StatusOK).
  881. Body(suite.marshal(([]cache.Document{documents[0], documents[1], documents[2], documents[4]}))).
  882. End()
  883. apitest.New().
  884. Handler(suite.handler).
  885. Get(operator.URL).
  886. Header("X-API-Key", apiKey).
  887. QueryParams(map[string]string{
  888. "user-id": "0",
  889. "offset": "0",
  890. "n": "0"}).
  891. Expect(t).
  892. Status(http.StatusOK).
  893. Body(suite.marshal(([]cache.Document{documents[0], documents[2], documents[4]}))).
  894. End()
  895. })
  896. }
  897. }
  898. func (suite *ServerTestSuite) TestDeleteFeedback() {
  899. t := suite.T()
  900. // Insert feedback
  901. feedback := []data.Feedback{
  902. {FeedbackKey: data.FeedbackKey{FeedbackType: "type1", UserId: "2", ItemId: "3"}},
  903. {FeedbackKey: data.FeedbackKey{FeedbackType: "type2", UserId: "2", ItemId: "3"}},
  904. {FeedbackKey: data.FeedbackKey{FeedbackType: "type3", UserId: "2", ItemId: "3"}},
  905. {FeedbackKey: data.FeedbackKey{FeedbackType: "type1", UserId: "1", ItemId: "6"}},
  906. {FeedbackKey: data.FeedbackKey{FeedbackType: "type1", UserId: "4", ItemId: "8"}},
  907. }
  908. apitest.New().
  909. Handler(suite.handler).
  910. Post("/api/feedback").
  911. Header("X-API-Key", apiKey).
  912. JSON(feedback).
  913. Expect(t).
  914. Status(http.StatusOK).
  915. Body(`{"RowAffected": 5}`).
  916. End()
  917. // Get Feedback
  918. apitest.New().
  919. Handler(suite.handler).
  920. Get("/api/feedback/2/3").
  921. Header("X-API-Key", apiKey).
  922. Expect(t).
  923. Body(suite.marshal([]data.Feedback{feedback[0], feedback[1], feedback[2]})).
  924. Status(http.StatusOK).
  925. End()
  926. // Get typed feedback
  927. apitest.New().
  928. Handler(suite.handler).
  929. Get("/api/feedback/type2/2/3").
  930. Header("X-API-Key", apiKey).
  931. Expect(t).
  932. Status(http.StatusOK).
  933. Body(suite.marshal(feedback[1])).
  934. End()
  935. // delete feedback
  936. apitest.New().
  937. Handler(suite.handler).
  938. Delete("/api/feedback/2/3").
  939. Header("X-API-Key", apiKey).
  940. Expect(t).
  941. Body(`{"RowAffected": 3}`).
  942. Status(http.StatusOK).
  943. End()
  944. // delete typed feedback
  945. apitest.New().
  946. Handler(suite.handler).
  947. Delete("/api/feedback/type1/4/8").
  948. Header("X-API-Key", apiKey).
  949. Expect(t).
  950. Status(http.StatusOK).
  951. Body(`{"RowAffected": 1}`).
  952. End()
  953. }
  954. func (suite *ServerTestSuite) TestTimeSeries() {
  955. ctx := context.Background()
  956. t := suite.T()
  957. timestamp := time.Now().Add(24 * time.Hour * -2)
  958. points := []cache.TimeSeriesPoint{
  959. {"Test_NDCG", timestamp.Add(24 * time.Hour * 0), 0},
  960. {"Test_NDCG", timestamp.Add(24 * time.Hour * 1), 1},
  961. {"Test_NDCG", timestamp.Add(24 * time.Hour * 2), 2},
  962. {"Test_NDCG", timestamp.Add(24 * time.Hour * 3), 3},
  963. {"Test_NDCG", timestamp.Add(24 * time.Hour * 4), 4},
  964. {"Test_Recall", timestamp, 1},
  965. }
  966. err := suite.CacheClient.AddTimeSeriesPoints(ctx, points)
  967. assert.NoError(t, err)
  968. apitest.New().
  969. Handler(suite.handler).
  970. Get("/api/measurements/Test_NDCG").
  971. Query("n", "3").
  972. Header("X-API-Key", apiKey).
  973. Expect(t).
  974. Status(http.StatusOK).
  975. Body(suite.marshal([]cache.TimeSeriesPoint{
  976. points[0],
  977. points[1],
  978. points[2],
  979. })).
  980. End()
  981. // test auth fail
  982. apitest.New().
  983. Handler(suite.handler).
  984. Get("/api/measurements/Test_NDCG").
  985. Query("n", "3").
  986. Expect(t).
  987. Status(http.StatusUnauthorized).
  988. End()
  989. }
  990. func (suite *ServerTestSuite) TestGetRecommends() {
  991. ctx := context.Background()
  992. t := suite.T()
  993. // insert hidden items
  994. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{{Id: "0", Score: 100, Categories: []string{""}}})
  995. assert.NoError(t, err)
  996. // hide item
  997. apitest.New().
  998. Handler(suite.handler).
  999. Patch("/api/item/0").
  1000. Header("X-API-Key", apiKey).
  1001. JSON(data.ItemPatch{IsHidden: proto.Bool(true)}).
  1002. Expect(t).
  1003. Status(http.StatusOK).
  1004. End()
  1005. // insert items
  1006. err = suite.DataClient.BatchInsertItems(ctx, []data.Item{
  1007. {ItemId: "1"},
  1008. {ItemId: "2"},
  1009. {ItemId: "3"},
  1010. {ItemId: "4"},
  1011. {ItemId: "5"},
  1012. {ItemId: "6"},
  1013. {ItemId: "7"},
  1014. {ItemId: "8"},
  1015. })
  1016. assert.NoError(t, err)
  1017. // insert recommendation
  1018. err = suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1019. {Id: "1", Score: 99, Categories: []string{""}},
  1020. {Id: "2", Score: 98, Categories: []string{""}},
  1021. {Id: "3", Score: 97, Categories: []string{""}},
  1022. {Id: "4", Score: 96, Categories: []string{""}},
  1023. {Id: "5", Score: 95, Categories: []string{""}},
  1024. {Id: "6", Score: 94, Categories: []string{""}},
  1025. {Id: "7", Score: 93, Categories: []string{""}},
  1026. {Id: "8", Score: 92, Categories: []string{""}},
  1027. })
  1028. assert.NoError(t, err)
  1029. // insert feedback
  1030. feedback := []data.Feedback{
  1031. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "2"}},
  1032. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "4"}},
  1033. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "1"}, Timestamp: time.Now().Add(time.Hour)},
  1034. }
  1035. apitest.New().
  1036. Handler(suite.handler).
  1037. Post("/api/feedback").
  1038. Header("X-API-Key", apiKey).
  1039. JSON(feedback).
  1040. Expect(t).
  1041. Status(http.StatusOK).
  1042. Body(`{"RowAffected": 3}`).
  1043. End()
  1044. apitest.New().
  1045. Handler(suite.handler).
  1046. Get("/api/recommend/0").
  1047. Header("X-API-Key", apiKey).
  1048. QueryParams(map[string]string{
  1049. "n": "3",
  1050. }).
  1051. Expect(t).
  1052. Status(http.StatusOK).
  1053. Body(suite.marshal([]string{"1", "3", "5"})).
  1054. End()
  1055. apitest.New().
  1056. Handler(suite.handler).
  1057. Get("/api/recommend/0").
  1058. Header("X-API-Key", apiKey).
  1059. QueryParams(map[string]string{
  1060. "n": "3",
  1061. "offset": "3",
  1062. }).
  1063. Expect(t).
  1064. Status(http.StatusOK).
  1065. Body(suite.marshal([]string{"6", "7", "8"})).
  1066. End()
  1067. apitest.New().
  1068. Handler(suite.handler).
  1069. Get("/api/recommend/0").
  1070. Header("X-API-Key", apiKey).
  1071. QueryParams(map[string]string{
  1072. "n": "3",
  1073. "offset": "10000",
  1074. }).
  1075. Expect(t).
  1076. Status(http.StatusOK).
  1077. Body(suite.marshal([]string{})).
  1078. End()
  1079. apitest.New().
  1080. Handler(suite.handler).
  1081. Get("/api/recommend/0").
  1082. Header("X-API-Key", apiKey).
  1083. QueryParams(map[string]string{
  1084. "n": "3",
  1085. "write-back-type": "read",
  1086. }).
  1087. Expect(t).
  1088. Status(http.StatusOK).
  1089. Body(suite.marshal([]string{"1", "3", "5"})).
  1090. End()
  1091. apitest.New().
  1092. Handler(suite.handler).
  1093. Get("/api/recommend/0").
  1094. Header("X-API-Key", apiKey).
  1095. QueryParams(map[string]string{
  1096. "n": "3",
  1097. "write-back-type": "read",
  1098. "write-back-delay": "10m",
  1099. }).
  1100. Expect(t).
  1101. Status(http.StatusOK).
  1102. Body(suite.marshal([]string{"6", "7", "8"})).
  1103. End()
  1104. apitest.New().
  1105. Handler(suite.handler).
  1106. Get("/api/recommend/0").
  1107. Header("X-API-Key", apiKey).
  1108. QueryParams(map[string]string{
  1109. "n": "3",
  1110. }).
  1111. Expect(t).
  1112. Status(http.StatusOK).
  1113. Body(suite.marshal([]string{"6", "7", "8"})).
  1114. End()
  1115. }
  1116. func (suite *ServerTestSuite) TestGetRecommendsWithMultiCategories() {
  1117. ctx := context.Background()
  1118. t := suite.T()
  1119. // insert recommendation
  1120. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1121. {Id: "1", Score: 1, Categories: []string{""}},
  1122. {Id: "2", Score: 2, Categories: []string{"", "2"}},
  1123. {Id: "3", Score: 3, Categories: []string{"", "3"}},
  1124. {Id: "4", Score: 4, Categories: []string{"", "2"}},
  1125. {Id: "5", Score: 5, Categories: []string{"", "5"}},
  1126. {Id: "6", Score: 6, Categories: []string{"", "2", "3"}},
  1127. {Id: "7", Score: 7, Categories: []string{"", "7"}},
  1128. {Id: "8", Score: 8, Categories: []string{"", "2"}},
  1129. {Id: "9", Score: 9, Categories: []string{"", "3"}},
  1130. })
  1131. suite.NoError(err)
  1132. apitest.New().
  1133. Handler(suite.handler).
  1134. Get("/api/recommend/0").
  1135. Header("X-API-Key", apiKey).
  1136. QueryCollection(map[string][]string{
  1137. "n": []string{"3"},
  1138. "category": []string{"2", "3"},
  1139. }).
  1140. Expect(t).
  1141. Status(http.StatusOK).
  1142. Body(suite.marshal([]string{"6"})).
  1143. End()
  1144. }
  1145. func (suite *ServerTestSuite) TestGetRecommendsWithReplacement() {
  1146. ctx := context.Background()
  1147. t := suite.T()
  1148. suite.Config.Recommend.Replacement.EnableReplacement = true
  1149. // insert recommendation
  1150. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1151. {Id: "0", Score: 100, Categories: []string{""}},
  1152. {Id: "1", Score: 99, Categories: []string{""}},
  1153. {Id: "2", Score: 98, Categories: []string{""}},
  1154. {Id: "3", Score: 97, Categories: []string{""}},
  1155. {Id: "4", Score: 96, Categories: []string{""}},
  1156. {Id: "5", Score: 95, Categories: []string{""}},
  1157. {Id: "6", Score: 94, Categories: []string{""}},
  1158. {Id: "7", Score: 93, Categories: []string{""}},
  1159. {Id: "8", Score: 92, Categories: []string{""}},
  1160. })
  1161. assert.NoError(t, err)
  1162. // hide item
  1163. apitest.New().
  1164. Handler(suite.handler).
  1165. Patch("/api/item/0").
  1166. Header("X-API-Key", apiKey).
  1167. JSON(data.ItemPatch{IsHidden: proto.Bool(true)}).
  1168. Expect(t).
  1169. Status(http.StatusOK).
  1170. End()
  1171. // insert feedback
  1172. feedback := []data.Feedback{
  1173. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "2"}},
  1174. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "4"}},
  1175. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "1"}, Timestamp: time.Now().Add(time.Hour)},
  1176. }
  1177. apitest.New().
  1178. Handler(suite.handler).
  1179. Post("/api/feedback").
  1180. Header("X-API-Key", apiKey).
  1181. JSON(feedback).
  1182. Expect(t).
  1183. Status(http.StatusOK).
  1184. Body(`{"RowAffected": 3}`).
  1185. End()
  1186. apitest.New().
  1187. Handler(suite.handler).
  1188. Get("/api/recommend/0").
  1189. Header("X-API-Key", apiKey).
  1190. QueryParams(map[string]string{
  1191. "n": "3",
  1192. }).
  1193. Expect(t).
  1194. Status(http.StatusOK).
  1195. Body(suite.marshal([]string{"1", "2", "3"})).
  1196. End()
  1197. }
  1198. func (suite *ServerTestSuite) TestServerGetRecommendsFallbackItemBasedSimilar() {
  1199. ctx := context.Background()
  1200. t := suite.T()
  1201. suite.Config.Recommend.Online.NumFeedbackFallbackItemBased = 4
  1202. suite.Config.Recommend.DataSource.PositiveFeedbackTypes = []string{"a"}
  1203. // insert recommendation
  1204. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1205. {Id: "1", Score: 99},
  1206. {Id: "2", Score: 98},
  1207. {Id: "3", Score: 97},
  1208. {Id: "4", Score: 96}})
  1209. assert.NoError(t, err)
  1210. // insert feedback
  1211. feedback := []data.Feedback{
  1212. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "1"}, Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)},
  1213. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "2"}, Timestamp: time.Date(2009, 1, 1, 1, 1, 1, 1, time.UTC)},
  1214. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "3"}, Timestamp: time.Date(2008, 1, 1, 1, 1, 1, 1, time.UTC)},
  1215. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "4"}, Timestamp: time.Date(2007, 1, 1, 1, 1, 1, 1, time.UTC)},
  1216. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "5"}, Timestamp: time.Date(2006, 1, 1, 1, 1, 1, 1, time.UTC)},
  1217. }
  1218. apitest.New().
  1219. Handler(suite.handler).
  1220. Post("/api/feedback").
  1221. Header("X-API-Key", apiKey).
  1222. JSON(feedback).
  1223. Expect(t).
  1224. Status(http.StatusOK).
  1225. Body(`{"RowAffected": 5}`).
  1226. End()
  1227. // insert similar items
  1228. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "1", []cache.Document{
  1229. {Id: "2", Score: 100000, Categories: []string{""}},
  1230. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1231. })
  1232. assert.NoError(t, err)
  1233. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "2", []cache.Document{
  1234. {Id: "3", Score: 100000, Categories: []string{"", "*"}},
  1235. {Id: "8", Score: 1, Categories: []string{""}},
  1236. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1237. })
  1238. assert.NoError(t, err)
  1239. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "3", []cache.Document{
  1240. {Id: "4", Score: 100000, Categories: []string{""}},
  1241. {Id: "7", Score: 1, Categories: []string{"", "*"}},
  1242. {Id: "8", Score: 1, Categories: []string{""}},
  1243. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1244. })
  1245. assert.NoError(t, err)
  1246. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "4", []cache.Document{
  1247. {Id: "1", Score: 100000, Categories: []string{"", "*"}},
  1248. {Id: "6", Score: 1, Categories: []string{""}},
  1249. {Id: "7", Score: 1, Categories: []string{"", "*"}},
  1250. {Id: "8", Score: 1, Categories: []string{""}},
  1251. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1252. })
  1253. assert.NoError(t, err)
  1254. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "5", []cache.Document{
  1255. {Id: "1", Score: 1, Categories: []string{""}},
  1256. {Id: "6", Score: 1, Categories: []string{""}},
  1257. {Id: "7", Score: 100000, Categories: []string{""}},
  1258. {Id: "8", Score: 100, Categories: []string{""}},
  1259. {Id: "9", Score: 1, Categories: []string{""}},
  1260. })
  1261. assert.NoError(t, err)
  1262. // test fallback
  1263. suite.Config.Recommend.Online.FallbackRecommend = []string{"item_based"}
  1264. apitest.New().
  1265. Handler(suite.handler).
  1266. Get("/api/recommend/0").
  1267. Header("X-API-Key", apiKey).
  1268. QueryParams(map[string]string{
  1269. "n": "3",
  1270. }).
  1271. Expect(t).
  1272. Status(http.StatusOK).
  1273. Body(suite.marshal([]string{"9", "8", "7"})).
  1274. End()
  1275. suite.Config.Recommend.Online.FallbackRecommend = []string{"item_based"}
  1276. apitest.New().
  1277. Handler(suite.handler).
  1278. Get("/api/recommend/0/*").
  1279. Header("X-API-Key", apiKey).
  1280. QueryParams(map[string]string{
  1281. "n": "3",
  1282. }).
  1283. Expect(t).
  1284. Status(http.StatusOK).
  1285. Body(suite.marshal([]string{"9", "7"})).
  1286. End()
  1287. }
  1288. func (suite *ServerTestSuite) TestGetRecommendsFallbackUserBasedSimilar() {
  1289. ctx := context.Background()
  1290. t := suite.T()
  1291. // insert recommendation
  1292. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0",
  1293. []cache.Document{{Id: "1", Score: 99}, {Id: "2", Score: 98}, {Id: "3", Score: 97}, {Id: "4", Score: 96}})
  1294. assert.NoError(t, err)
  1295. // insert feedback
  1296. feedback := []data.Feedback{
  1297. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "1"}},
  1298. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "2"}},
  1299. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "3"}},
  1300. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "4"}},
  1301. }
  1302. apitest.New().
  1303. Handler(suite.handler).
  1304. Post("/api/feedback").
  1305. Header("X-API-Key", apiKey).
  1306. JSON(feedback).
  1307. Expect(t).
  1308. Status(http.StatusOK).
  1309. Body(`{"RowAffected": 4}`).
  1310. End()
  1311. // insert similar users
  1312. err = suite.CacheClient.AddDocuments(ctx, cache.UserNeighbors, "0", []cache.Document{
  1313. {Id: "1", Score: 2, Categories: []string{""}},
  1314. {Id: "2", Score: 1.5, Categories: []string{""}},
  1315. {Id: "3", Score: 1, Categories: []string{""}},
  1316. })
  1317. assert.NoError(t, err)
  1318. err = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{
  1319. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "1", ItemId: "11"}},
  1320. }, true, true, true)
  1321. assert.NoError(t, err)
  1322. err = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{
  1323. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "2", ItemId: "12"}},
  1324. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "2", ItemId: "48"}},
  1325. }, true, true, true)
  1326. assert.NoError(t, err)
  1327. err = suite.DataClient.BatchInsertFeedback(ctx, []data.Feedback{
  1328. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "3", ItemId: "13"}},
  1329. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "3", ItemId: "48"}},
  1330. }, true, true, true)
  1331. assert.NoError(t, err)
  1332. // insert categorized items
  1333. err = suite.DataClient.BatchInsertItems(ctx, []data.Item{
  1334. {ItemId: "12", Categories: []string{"*"}},
  1335. {ItemId: "48", Categories: []string{"*"}},
  1336. })
  1337. assert.NoError(t, err)
  1338. // test fallback
  1339. suite.Config.Recommend.Online.FallbackRecommend = []string{"user_based"}
  1340. apitest.New().
  1341. Handler(suite.handler).
  1342. Get("/api/recommend/0").
  1343. Header("X-API-Key", apiKey).
  1344. QueryParams(map[string]string{
  1345. "n": "3",
  1346. }).
  1347. Expect(t).
  1348. Status(http.StatusOK).
  1349. Body(suite.marshal([]string{"48", "11", "12"})).
  1350. End()
  1351. apitest.New().
  1352. Handler(suite.handler).
  1353. Get("/api/recommend/0/*").
  1354. Header("X-API-Key", apiKey).
  1355. QueryParams(map[string]string{
  1356. "n": "3",
  1357. }).
  1358. Expect(t).
  1359. Status(http.StatusOK).
  1360. Body(suite.marshal([]string{"48", "12"})).
  1361. End()
  1362. }
  1363. func (suite *ServerTestSuite) TestGetRecommendsFallbackPreCached() {
  1364. ctx := context.Background()
  1365. t := suite.T()
  1366. // insert offline recommendation
  1367. err := suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1368. {Id: "1", Score: 99, Categories: []string{""}},
  1369. {Id: "2", Score: 98, Categories: []string{""}},
  1370. {Id: "3", Score: 97, Categories: []string{""}},
  1371. {Id: "4", Score: 96, Categories: []string{""}}})
  1372. assert.NoError(t, err)
  1373. err = suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "0", []cache.Document{
  1374. {Id: "101", Score: 99, Categories: []string{"*"}},
  1375. {Id: "102", Score: 98, Categories: []string{"*"}},
  1376. {Id: "103", Score: 97, Categories: []string{"*"}},
  1377. {Id: "104", Score: 96, Categories: []string{"*"}}})
  1378. assert.NoError(t, err)
  1379. // insert latest
  1380. err = suite.CacheClient.AddDocuments(ctx, cache.LatestItems, "", []cache.Document{
  1381. {Id: "5", Score: 95, Categories: []string{""}},
  1382. {Id: "6", Score: 94, Categories: []string{""}},
  1383. {Id: "7", Score: 93, Categories: []string{""}},
  1384. {Id: "8", Score: 92, Categories: []string{""}}})
  1385. assert.NoError(t, err)
  1386. err = suite.CacheClient.AddDocuments(ctx, cache.LatestItems, "", []cache.Document{
  1387. {Id: "105", Score: 95, Categories: []string{"*"}},
  1388. {Id: "106", Score: 94, Categories: []string{"*"}},
  1389. {Id: "107", Score: 93, Categories: []string{"*"}},
  1390. {Id: "108", Score: 92, Categories: []string{"*"}}})
  1391. assert.NoError(t, err)
  1392. // insert popular
  1393. err = suite.CacheClient.AddDocuments(ctx, cache.PopularItems, "", []cache.Document{
  1394. {Id: "9", Score: 91, Categories: []string{""}},
  1395. {Id: "10", Score: 90, Categories: []string{""}},
  1396. {Id: "11", Score: 89, Categories: []string{""}},
  1397. {Id: "12", Score: 88, Categories: []string{""}}})
  1398. assert.NoError(t, err)
  1399. err = suite.CacheClient.AddDocuments(ctx, cache.PopularItems, "", []cache.Document{
  1400. {Id: "109", Score: 91, Categories: []string{"*"}},
  1401. {Id: "110", Score: 90, Categories: []string{"*"}},
  1402. {Id: "111", Score: 89, Categories: []string{"*"}},
  1403. {Id: "112", Score: 88, Categories: []string{"*"}}})
  1404. assert.NoError(t, err)
  1405. // insert collaborative filtering
  1406. err = suite.CacheClient.AddDocuments(ctx, cache.CollaborativeRecommend, "0", []cache.Document{
  1407. {Id: "13", Score: 79, Categories: []string{""}},
  1408. {Id: "14", Score: 78, Categories: []string{""}},
  1409. {Id: "15", Score: 77, Categories: []string{""}},
  1410. {Id: "16", Score: 76, Categories: []string{""}}})
  1411. assert.NoError(t, err)
  1412. err = suite.CacheClient.AddDocuments(ctx, cache.CollaborativeRecommend, "0", []cache.Document{
  1413. {Id: "113", Score: 79, Categories: []string{"*"}},
  1414. {Id: "114", Score: 78, Categories: []string{"*"}},
  1415. {Id: "115", Score: 77, Categories: []string{"*"}},
  1416. {Id: "116", Score: 76, Categories: []string{"*"}}})
  1417. assert.NoError(t, err)
  1418. // test popular fallback
  1419. suite.Config.Recommend.Online.FallbackRecommend = []string{"popular"}
  1420. apitest.New().
  1421. Handler(suite.handler).
  1422. Get("/api/recommend/0").
  1423. Header("X-API-Key", apiKey).
  1424. QueryParams(map[string]string{
  1425. "n": "8",
  1426. }).
  1427. Expect(t).
  1428. Status(http.StatusOK).
  1429. Body(suite.marshal([]string{"1", "2", "3", "4", "9", "10", "11", "12"})).
  1430. End()
  1431. apitest.New().
  1432. Handler(suite.handler).
  1433. Get("/api/recommend/0/*").
  1434. Header("X-API-Key", apiKey).
  1435. QueryParams(map[string]string{
  1436. "n": "8",
  1437. }).
  1438. Expect(t).
  1439. Status(http.StatusOK).
  1440. Body(suite.marshal([]string{"101", "102", "103", "104", "109", "110", "111", "112"})).
  1441. End()
  1442. // test latest fallback
  1443. suite.Config.Recommend.Online.FallbackRecommend = []string{"latest"}
  1444. apitest.New().
  1445. Handler(suite.handler).
  1446. Get("/api/recommend/0").
  1447. Header("X-API-Key", apiKey).
  1448. QueryParams(map[string]string{
  1449. "n": "8",
  1450. }).
  1451. Expect(t).
  1452. Status(http.StatusOK).
  1453. Body(suite.marshal([]string{"1", "2", "3", "4", "5", "6", "7", "8"})).
  1454. End()
  1455. apitest.New().
  1456. Handler(suite.handler).
  1457. Get("/api/recommend/0/*").
  1458. Header("X-API-Key", apiKey).
  1459. QueryParams(map[string]string{
  1460. "n": "8",
  1461. }).
  1462. Expect(t).
  1463. Status(http.StatusOK).
  1464. Body(suite.marshal([]string{"101", "102", "103", "104", "105", "106", "107", "108"})).
  1465. End()
  1466. // test collaborative filtering
  1467. suite.Config.Recommend.Online.FallbackRecommend = []string{"collaborative"}
  1468. apitest.New().
  1469. Handler(suite.handler).
  1470. Get("/api/recommend/0").
  1471. Header("X-API-Key", apiKey).
  1472. QueryParams(map[string]string{
  1473. "n": "8",
  1474. }).
  1475. Expect(t).
  1476. Status(http.StatusOK).
  1477. Body(suite.marshal([]string{"1", "2", "3", "4", "13", "14", "15", "16"})).
  1478. End()
  1479. apitest.New().
  1480. Handler(suite.handler).
  1481. Get("/api/recommend/0/*").
  1482. Header("X-API-Key", apiKey).
  1483. QueryParams(map[string]string{
  1484. "n": "8",
  1485. }).
  1486. Expect(t).
  1487. Status(http.StatusOK).
  1488. Body(suite.marshal([]string{"101", "102", "103", "104", "113", "114", "115", "116"})).
  1489. End()
  1490. // test wrong fallback
  1491. suite.Config.Recommend.Online.FallbackRecommend = []string{""}
  1492. apitest.New().
  1493. Handler(suite.handler).
  1494. Get("/api/recommend/0").
  1495. Header("X-API-Key", apiKey).
  1496. QueryParams(map[string]string{
  1497. "n": "8",
  1498. }).
  1499. Expect(t).
  1500. Status(http.StatusInternalServerError).
  1501. End()
  1502. }
  1503. func (suite *ServerTestSuite) TestSessionRecommend() {
  1504. ctx := context.Background()
  1505. t := suite.T()
  1506. suite.Config.Recommend.Online.NumFeedbackFallbackItemBased = 4
  1507. suite.Config.Recommend.DataSource.PositiveFeedbackTypes = []string{"a"}
  1508. // insert similar items
  1509. err := suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "1", []cache.Document{
  1510. {Id: "2", Score: 100000, Categories: []string{""}},
  1511. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1512. {Id: "100", Score: 100000, Categories: []string{""}},
  1513. })
  1514. assert.NoError(t, err)
  1515. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "2", []cache.Document{
  1516. {Id: "3", Score: 100000, Categories: []string{"", "*"}},
  1517. {Id: "8", Score: 1, Categories: []string{""}},
  1518. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1519. })
  1520. assert.NoError(t, err)
  1521. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "3", []cache.Document{
  1522. {Id: "4", Score: 100000, Categories: []string{""}},
  1523. {Id: "7", Score: 1, Categories: []string{"", "*"}},
  1524. {Id: "8", Score: 1, Categories: []string{""}},
  1525. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1526. })
  1527. assert.NoError(t, err)
  1528. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "4", []cache.Document{
  1529. {Id: "1", Score: 100000, Categories: []string{"", "*"}},
  1530. {Id: "6", Score: 1, Categories: []string{""}},
  1531. {Id: "7", Score: 1, Categories: []string{"", "*"}},
  1532. {Id: "8", Score: 1, Categories: []string{""}},
  1533. {Id: "9", Score: 1, Categories: []string{"", "*"}},
  1534. })
  1535. assert.NoError(t, err)
  1536. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "5", []cache.Document{
  1537. {Id: "1", Score: 1, Categories: []string{""}},
  1538. {Id: "6", Score: 1, Categories: []string{""}},
  1539. {Id: "7", Score: 100000, Categories: []string{""}},
  1540. {Id: "8", Score: 100, Categories: []string{""}},
  1541. {Id: "9", Score: 1, Categories: []string{""}},
  1542. })
  1543. assert.NoError(t, err)
  1544. // hide items
  1545. apitest.New().
  1546. Handler(suite.handler).
  1547. Post("/api/item").
  1548. Header("X-API-Key", apiKey).
  1549. JSON(Item{ItemId: "100", IsHidden: true}).
  1550. Expect(t).
  1551. Status(http.StatusOK).
  1552. Body(`{"RowAffected": 1}`).
  1553. End()
  1554. // test fallback
  1555. feedback := []data.Feedback{
  1556. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "1"}, Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)},
  1557. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "2"}, Timestamp: time.Date(2009, 1, 1, 1, 1, 1, 1, time.UTC)},
  1558. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "3"}, Timestamp: time.Date(2008, 1, 1, 1, 1, 1, 1, time.UTC)},
  1559. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "4"}, Timestamp: time.Date(2007, 1, 1, 1, 1, 1, 1, time.UTC)},
  1560. {FeedbackKey: data.FeedbackKey{FeedbackType: "a", UserId: "0", ItemId: "5"}, Timestamp: time.Date(2006, 1, 1, 1, 1, 1, 1, time.UTC)},
  1561. }
  1562. apitest.New().
  1563. Handler(suite.handler).
  1564. Post("/api/session/recommend").
  1565. Header("X-API-Key", apiKey).
  1566. QueryParams(map[string]string{
  1567. "n": "3",
  1568. }).
  1569. JSON(feedback).
  1570. Expect(t).
  1571. Status(http.StatusOK).
  1572. Body(suite.marshal([]cache.Document{{Id: "9", Score: 4}, {Id: "8", Score: 3}, {Id: "7", Score: 2}})).
  1573. End()
  1574. apitest.New().
  1575. Handler(suite.handler).
  1576. Post("/api/session/recommend").
  1577. Header("X-API-Key", apiKey).
  1578. QueryParams(map[string]string{
  1579. "offset": "100",
  1580. }).
  1581. JSON(feedback).
  1582. Expect(t).
  1583. Status(http.StatusOK).
  1584. Body(suite.marshal([]cache.Document(nil))).
  1585. End()
  1586. suite.Config.Recommend.Online.FallbackRecommend = []string{"item_based"}
  1587. apitest.New().
  1588. Handler(suite.handler).
  1589. Post("/api/session/recommend/*").
  1590. Header("X-API-Key", apiKey).
  1591. QueryParams(map[string]string{
  1592. "n": "3",
  1593. }).
  1594. JSON(feedback).
  1595. Expect(t).
  1596. Status(http.StatusOK).
  1597. Body(suite.marshal([]cache.Document{{Id: "9", Score: 4}, {Id: "7", Score: 2}})).
  1598. End()
  1599. }
  1600. func (suite *ServerTestSuite) TestVisibility() {
  1601. ctx := context.Background()
  1602. t := suite.T()
  1603. // insert items: 0, 1, 2, 3, 4
  1604. var items []Item
  1605. for i := 0; i < 5; i++ {
  1606. items = append(items, Item{
  1607. ItemId: strconv.Itoa(i),
  1608. Categories: []string{"a"},
  1609. Timestamp: time.Date(1989, 6, i+1, 1, 1, 1, 1, time.UTC).String(),
  1610. })
  1611. }
  1612. apitest.New().
  1613. Handler(suite.handler).
  1614. Post("/api/items").
  1615. Header("X-API-Key", apiKey).
  1616. QueryParams(map[string]string{
  1617. "n": "3",
  1618. }).
  1619. JSON(items).
  1620. Expect(t).
  1621. Status(http.StatusOK).
  1622. End()
  1623. // insert cache
  1624. var documents []cache.Document
  1625. for i := range items {
  1626. documents = append(documents, cache.Document{
  1627. Id: strconv.Itoa(i),
  1628. Score: float64(time.Date(1989, 6, i+1, 1, 1, 1, 1, time.UTC).Unix()),
  1629. Categories: []string{"", "a"},
  1630. })
  1631. }
  1632. lo.Reverse(documents)
  1633. err := suite.CacheClient.AddDocuments(ctx, cache.LatestItems, "", documents)
  1634. assert.NoError(t, err)
  1635. err = suite.CacheClient.AddDocuments(ctx, cache.PopularItems, "", documents)
  1636. assert.NoError(t, err)
  1637. err = suite.CacheClient.AddDocuments(ctx, cache.ItemNeighbors, "100", documents)
  1638. assert.NoError(t, err)
  1639. err = suite.CacheClient.AddDocuments(ctx, cache.OfflineRecommend, "100", documents)
  1640. assert.NoError(t, err)
  1641. // delete item
  1642. apitest.New().
  1643. Handler(suite.handler).
  1644. Delete("/api/item/0").
  1645. Header("X-API-Key", apiKey).
  1646. JSON(items).
  1647. Expect(t).
  1648. Status(http.StatusOK).
  1649. End()
  1650. // modify item
  1651. apitest.New().
  1652. Handler(suite.handler).
  1653. Patch("/api/item/1").
  1654. Header("X-API-Key", apiKey).
  1655. JSON(data.ItemPatch{IsHidden: proto.Bool(true)}).
  1656. Expect(t).
  1657. Status(http.StatusOK).
  1658. End()
  1659. // overwrite item
  1660. apitest.New().
  1661. Handler(suite.handler).
  1662. Post("/api/item").
  1663. Header("X-API-Key", apiKey).
  1664. JSON(Item{ItemId: "2", IsHidden: true}).
  1665. Expect(t).
  1666. Status(http.StatusOK).
  1667. End()
  1668. // recommend
  1669. apitest.New().
  1670. Handler(suite.handler).
  1671. Get("/api/popular").
  1672. Header("X-API-Key", apiKey).
  1673. JSON(items).
  1674. Expect(t).
  1675. Status(http.StatusOK).
  1676. Body(suite.marshal((documents[:2]))).
  1677. End()
  1678. apitest.New().
  1679. Handler(suite.handler).
  1680. Get("/api/latest").
  1681. Header("X-API-Key", apiKey).
  1682. JSON(items).
  1683. Expect(t).
  1684. Status(http.StatusOK).
  1685. Body(suite.marshal((documents[:2]))).
  1686. End()
  1687. apitest.New().
  1688. Handler(suite.handler).
  1689. Get("/api/item/100/neighbors/").
  1690. Header("X-API-Key", apiKey).
  1691. JSON(items).
  1692. Expect(t).
  1693. Status(http.StatusOK).
  1694. Body(suite.marshal((documents[:2]))).
  1695. End()
  1696. apitest.New().
  1697. Handler(suite.handler).
  1698. Get("/api/recommend/100/").
  1699. Header("X-API-Key", apiKey).
  1700. JSON(items).
  1701. Expect(t).
  1702. Status(http.StatusOK).
  1703. Body(suite.marshal(cache.ConvertDocumentsToValues(documents[:2]))).
  1704. End()
  1705. // insert item
  1706. apitest.New().
  1707. Handler(suite.handler).
  1708. Post("/api/item").
  1709. Header("X-API-Key", apiKey).
  1710. JSON(Item{ItemId: "0", Timestamp: time.Date(1989, 6, 1, 1, 1, 1, 1, time.UTC).String()}).
  1711. Expect(t).
  1712. Status(http.StatusOK).
  1713. End()
  1714. // modify item
  1715. apitest.New().
  1716. Handler(suite.handler).
  1717. Patch("/api/item/1").
  1718. Header("X-API-Key", apiKey).
  1719. JSON(data.ItemPatch{IsHidden: proto.Bool(false)}).
  1720. Expect(t).
  1721. Status(http.StatusOK).
  1722. End()
  1723. // overwrite item
  1724. apitest.New().
  1725. Handler(suite.handler).
  1726. Post("/api/item").
  1727. Header("X-API-Key", apiKey).
  1728. JSON(Item{ItemId: "2", IsHidden: false, Timestamp: time.Date(1989, 6, 3, 1, 1, 1, 1, time.UTC).String()}).
  1729. Expect(t).
  1730. Status(http.StatusOK).
  1731. End()
  1732. // recommend
  1733. apitest.New().
  1734. Handler(suite.handler).
  1735. Get("/api/popular").
  1736. Header("X-API-Key", apiKey).
  1737. JSON(items).
  1738. Expect(t).
  1739. Status(http.StatusOK).
  1740. Body(suite.marshal(documents[:len(documents)-1])).
  1741. End()
  1742. apitest.New().
  1743. Handler(suite.handler).
  1744. Get("/api/latest").
  1745. Header("X-API-Key", apiKey).
  1746. JSON(items).
  1747. Expect(t).
  1748. Status(http.StatusOK).
  1749. Body(suite.marshal(documents)).
  1750. End()
  1751. apitest.New().
  1752. Handler(suite.handler).
  1753. Get("/api/item/100/neighbors/").
  1754. Header("X-API-Key", apiKey).
  1755. JSON(items).
  1756. Expect(t).
  1757. Status(http.StatusOK).
  1758. Body(suite.marshal(documents[:len(documents)-1])).
  1759. End()
  1760. apitest.New().
  1761. Handler(suite.handler).
  1762. Get("/api/recommend/100/").
  1763. Header("X-API-Key", apiKey).
  1764. JSON(items).
  1765. Expect(t).
  1766. Status(http.StatusOK).
  1767. Body(suite.marshal(cache.ConvertDocumentsToValues(documents))).
  1768. End()
  1769. // delete category
  1770. apitest.New().
  1771. Handler(suite.handler).
  1772. Delete("/api/item/0/category/a").
  1773. Header("X-API-Key", apiKey).
  1774. JSON(items).
  1775. Expect(t).
  1776. Status(http.StatusOK).
  1777. End()
  1778. // modify category
  1779. apitest.New().
  1780. Handler(suite.handler).
  1781. Patch("/api/item/1").
  1782. Header("X-API-Key", apiKey).
  1783. JSON(data.ItemPatch{Categories: []string{}}).
  1784. Expect(t).
  1785. Status(http.StatusOK).
  1786. End()
  1787. // overwrite category
  1788. apitest.New().
  1789. Handler(suite.handler).
  1790. Post("/api/item").
  1791. Header("X-API-Key", apiKey).
  1792. JSON(Item{ItemId: "2", Categories: []string{}}).
  1793. Expect(t).
  1794. Status(http.StatusOK).
  1795. End()
  1796. // recommend
  1797. apitest.New().
  1798. Handler(suite.handler).
  1799. Get("/api/popular/a").
  1800. Header("X-API-Key", apiKey).
  1801. JSON(items).
  1802. Expect(t).
  1803. Status(http.StatusOK).
  1804. Body(suite.marshal(documents[:2])).
  1805. End()
  1806. apitest.New().
  1807. Handler(suite.handler).
  1808. Get("/api/latest/a").
  1809. Header("X-API-Key", apiKey).
  1810. JSON(items).
  1811. Expect(t).
  1812. Status(http.StatusOK).
  1813. Body(suite.marshal(documents[:2])).
  1814. End()
  1815. apitest.New().
  1816. Handler(suite.handler).
  1817. Get("/api/item/100/neighbors/a").
  1818. Header("X-API-Key", apiKey).
  1819. JSON(items).
  1820. Expect(t).
  1821. Status(http.StatusOK).
  1822. Body(suite.marshal(documents[:2])).
  1823. End()
  1824. apitest.New().
  1825. Handler(suite.handler).
  1826. Get("/api/recommend/100/a").
  1827. Header("X-API-Key", apiKey).
  1828. JSON(items).
  1829. Expect(t).
  1830. Status(http.StatusOK).
  1831. Body(suite.marshal(cache.ConvertDocumentsToValues(documents[:2]))).
  1832. End()
  1833. // delete category
  1834. apitest.New().
  1835. Handler(suite.handler).
  1836. Put("/api/item/0/category/a").
  1837. Header("X-API-Key", apiKey).
  1838. JSON(items).
  1839. Expect(t).
  1840. Status(http.StatusOK).
  1841. End()
  1842. // modify category
  1843. apitest.New().
  1844. Handler(suite.handler).
  1845. Patch("/api/item/1").
  1846. Header("X-API-Key", apiKey).
  1847. JSON(data.ItemPatch{Categories: []string{"a"}}).
  1848. Expect(t).
  1849. Status(http.StatusOK).
  1850. End()
  1851. // overwrite category
  1852. apitest.New().
  1853. Handler(suite.handler).
  1854. Post("/api/item").
  1855. Header("X-API-Key", apiKey).
  1856. JSON(Item{ItemId: "2", Categories: []string{"a"}, Timestamp: time.Date(1989, 6, 3, 1, 1, 1, 1, time.UTC).String()}).
  1857. Expect(t).
  1858. Status(http.StatusOK).
  1859. End()
  1860. // recommend
  1861. apitest.New().
  1862. Handler(suite.handler).
  1863. Get("/api/popular/a").
  1864. Header("X-API-Key", apiKey).
  1865. JSON(items).
  1866. Expect(t).
  1867. Status(http.StatusOK).
  1868. Body(suite.marshal(documents[:len(documents)-1])).
  1869. End()
  1870. apitest.New().
  1871. Handler(suite.handler).
  1872. Get("/api/latest/a").
  1873. Header("X-API-Key", apiKey).
  1874. JSON(items).
  1875. Expect(t).
  1876. Status(http.StatusOK).
  1877. Body(suite.marshal(documents)).
  1878. End()
  1879. apitest.New().
  1880. Handler(suite.handler).
  1881. Get("/api/item/100/neighbors/a").
  1882. Header("X-API-Key", apiKey).
  1883. JSON(items).
  1884. Expect(t).
  1885. Status(http.StatusOK).
  1886. Body(suite.marshal(documents[:len(documents)-1])).
  1887. End()
  1888. apitest.New().
  1889. Handler(suite.handler).
  1890. Get("/api/recommend/100/a").
  1891. Header("X-API-Key", apiKey).
  1892. JSON(items).
  1893. Expect(t).
  1894. Status(http.StatusOK).
  1895. Body(suite.marshal(cache.ConvertDocumentsToValues(documents))).
  1896. End()
  1897. }
  1898. func (suite *ServerTestSuite) TestHealth() {
  1899. t := suite.T()
  1900. // ready
  1901. apitest.New().
  1902. Handler(suite.handler).
  1903. Get("/api/health/live").
  1904. Expect(t).
  1905. Status(http.StatusOK).
  1906. Body(suite.marshal(HealthStatus{
  1907. Ready: true,
  1908. DataStoreError: nil,
  1909. CacheStoreError: nil,
  1910. DataStoreConnected: true,
  1911. CacheStoreConnected: true,
  1912. })).
  1913. End()
  1914. apitest.New().
  1915. Handler(suite.handler).
  1916. Get("/api/health/ready").
  1917. Expect(t).
  1918. Status(http.StatusOK).
  1919. Body(suite.marshal(HealthStatus{
  1920. Ready: true,
  1921. DataStoreError: nil,
  1922. CacheStoreError: nil,
  1923. DataStoreConnected: true,
  1924. CacheStoreConnected: true,
  1925. })).
  1926. End()
  1927. // not ready
  1928. dataClient, cacheClient := suite.DataClient, suite.CacheClient
  1929. suite.DataClient, suite.CacheClient = data.NoDatabase{}, cache.NoDatabase{}
  1930. apitest.New().
  1931. Handler(suite.handler).
  1932. Get("/api/health/live").
  1933. Expect(t).
  1934. Status(http.StatusOK).
  1935. Body(suite.marshal(HealthStatus{
  1936. Ready: false,
  1937. DataStoreError: data.ErrNoDatabase,
  1938. CacheStoreError: cache.ErrNoDatabase,
  1939. DataStoreConnected: false,
  1940. CacheStoreConnected: false,
  1941. })).
  1942. End()
  1943. apitest.New().
  1944. Handler(suite.handler).
  1945. Get("/api/health/ready").
  1946. Expect(t).
  1947. Status(http.StatusServiceUnavailable).
  1948. Body(suite.marshal(HealthStatus{
  1949. Ready: false,
  1950. DataStoreError: data.ErrNoDatabase,
  1951. CacheStoreError: cache.ErrNoDatabase,
  1952. DataStoreConnected: false,
  1953. CacheStoreConnected: false,
  1954. })).
  1955. End()
  1956. suite.DataClient, suite.CacheClient = dataClient, cacheClient
  1957. }
  1958. func TestServer(t *testing.T) {
  1959. suite.Run(t, new(ServerTestSuite))
  1960. }