From eddfe9f0ca404f99da318a12c2626299d8da6372 Mon Sep 17 00:00:00 2001 From: MrPhotato <137085785+MrPhotato@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:50:43 +0800 Subject: [PATCH] feat: check wishlist by pages (#55) Co-authored-by: GuoHeZuYa <85046837+GuoHeZuYa@users.noreply.github.com> Co-authored-by: Jyf <1254023440@qq.com> Co-authored-by: amber <> Co-authored-by: Yuwang Cai --- .github/workflows/ci-wishlist-service.yaml | 47 +++++++ .github/workflows/provision.yaml | 1 + envs/account.env.gpg | 4 +- envs/currency.env.gpg | Bin 123 -> 123 bytes envs/item.env.gpg | Bin 391 -> 393 bytes envs/notification.env.gpg | Bin 183 -> 194 bytes envs/web.env.gpg | 3 +- envs/wishlist-mongo.env.gpg | Bin 171 -> 176 bytes envs/wishlist.env.gpg | Bin 282 -> 285 bytes helm/templates/wishlist/service.yaml | 4 +- services/item/Dockerfile | 6 +- services/item/Dockerfile.dev | 4 +- services/notification/Dockerfile | 2 + services/notification/main.go | 16 +++ services/web/Dockerfile | 2 + services/web/Dockerfile.dev | 4 +- services/web/src/app/healthz/route.ts | 3 + services/wishlist/Dockerfile | 11 +- services/wishlist/Dockerfile.dev | 6 + services/wishlist/Dockerfile.test | 8 ++ services/wishlist/database/1-schema.js | 10 +- services/wishlist/database/dev/1-schema.js | 14 +++ services/wishlist/database/dev/2-seed.js | 106 ++++++++++++++++ services/wishlist/database/test/1-schema.js | 35 ++++++ services/wishlist/database/test/2-seed.js | 106 ++++++++++++++++ services/wishlist/docker-compose.test.yaml | 28 +++++ services/wishlist/pom.xml | 118 +++++++++++------- .../market/controller/WishlistController.java | 24 +++- .../market/converter/ConvertDateToISO.java | 11 ++ .../java/edu/nus/market/dao/WishlistDao.java | 3 +- .../java/edu/nus/market/pojo/Account.java | 53 ++++++++ .../nus/market/pojo/ReqEntity/AddLikeReq.java | 2 + .../nus/market/pojo/ResEntity/JWTPayload.java | 24 ++++ .../nus/market/security/JwtTokenManager.java | 77 +++++++----- .../nus/market/service/WishlistService.java | 4 +- .../market/service/WishlistServiceImpl.java | 20 ++- .../src/main/resources/application.yml | 10 +- .../nus/market/WishlistControllerTest.java | 19 +-- .../java/edu/nus/market/WishlistDAOTest.java | 5 +- .../nus/market/WishlistServiceImplTest.java | 10 +- 40 files changed, 673 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/ci-wishlist-service.yaml create mode 100644 services/web/src/app/healthz/route.ts create mode 100644 services/wishlist/Dockerfile.dev create mode 100644 services/wishlist/Dockerfile.test create mode 100644 services/wishlist/database/dev/1-schema.js create mode 100644 services/wishlist/database/dev/2-seed.js create mode 100644 services/wishlist/database/test/1-schema.js create mode 100644 services/wishlist/database/test/2-seed.js create mode 100644 services/wishlist/docker-compose.test.yaml create mode 100644 services/wishlist/src/main/java/edu/nus/market/converter/ConvertDateToISO.java create mode 100644 services/wishlist/src/main/java/edu/nus/market/pojo/Account.java create mode 100644 services/wishlist/src/main/java/edu/nus/market/pojo/ResEntity/JWTPayload.java diff --git a/.github/workflows/ci-wishlist-service.yaml b/.github/workflows/ci-wishlist-service.yaml new file mode 100644 index 00000000..3c1af024 --- /dev/null +++ b/.github/workflows/ci-wishlist-service.yaml @@ -0,0 +1,47 @@ +name: CI for Wishlist Service + +on: + push: + branches: + - main + paths: + - services/wishlist/** + pull_request: + branches: + - main + paths: + - services/wishlist/** + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: services/wishlist + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Start containers + run: | + docker compose -f docker-compose.test.yaml up -d --build + + - name: Wait for test reports + run: | + for i in {1..30}; do + if [ -f ./reports/index.html ]; then + echo "Test report is ready!" + break + fi + echo "Waiting for test report to be generated..." + sleep 5 + done + ls -lart reports + + - name: Upload Test Reports + uses: actions/upload-artifact@v4 + with: + name: jacoco-reports + path: ${{ github.workspace }}/services/wishlist/reports diff --git a/.github/workflows/provision.yaml b/.github/workflows/provision.yaml index 3b0b0228..7a53f3f0 100644 --- a/.github/workflows/provision.yaml +++ b/.github/workflows/provision.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - NSHM-9-Set-up-environment workflow_dispatch: inputs: provision: diff --git a/envs/account.env.gpg b/envs/account.env.gpg index 414106c3..b857235f 100644 --- a/envs/account.env.gpg +++ b/envs/account.env.gpg @@ -1,2 +1,2 @@ -�  ��cՋ����FN>�naZz�����}���� y -d�e}��p�d<��ur�BǢP�@6`�AZ�l-�oÕɫ����R�O:�F)X�x�y�9�.���v�.��!��K��u_=�4oΦ�1�_x�8�@�q��b/��8g�G����;��!��+t�i�����w�[{A�% ,끡�� #(��~'߻]���&W$��W-_#X>��.?}�%�4`�.a����J��+�!�52y�M0�e�$�^�݉J��b}E��!h|쑰���� \ No newline at end of file +�  &RD�LșD���HK��p���ɞt2�y[(>�������J�hOB�]�]�P9�[�{2�_�8 �N��V��-��ϙ���������J1��s)_ⷐ ��\L�#��7&\��ϲw7�$t���,$�w�*��(`�P��\Mt����{3w- �z,��HU-�*�.t?��~ +÷�+*�+u�����8�r�|0bV ��|q�t���̵U#��q��e�7��p���Cu���q��LD�55IJ��+� }�g�wz�i�d{��V*ҟg \ No newline at end of file diff --git a/envs/currency.env.gpg b/envs/currency.env.gpg index a1a993f7b37c2ce5bd27722710954dfef3970a6c..01304d9b2e13a75ec8757f2be2025b4fdd6b1a0f 100644 GIT binary patch literal 123 zcmV->0EGXH4Fm}T2=j?t0N&!fjsMbW0fd>b_9MU{&9BW2r4GGr3h<{6YV7PF{jlm! z75+n)CRN`Uq{*bzpKJ`q7O*H~6IN|V&`AQTjBjPU_T;8w=eH3isF7=}yVACJpRv61 de;N+o1eXE9rA;*#S@1R#1Hp8{mFw-}ooKN0J@^0s literal 123 zcmV->0EGXH4Fm}T2rDwU61Dio-T%^R0T9=L0i#4WUw;_M(DH+7k3I~F_z!`qxG*9v z=`O43x0g@ta?%evLXiv9dhBas7~=8kwdcmt;~OV-hy#!m$Iq$8-5gXcfyiKaxsvJ` d0QNE%h$IfuIKh_SCyO>dV0oy%18kL;TXgH5I-LLj diff --git a/envs/item.env.gpg b/envs/item.env.gpg index deb56c25f2131e1b5bea357fc6386780c35caaea..bdde2547d3a82a90d6112deb2462fd6a3b5c4c60 100644 GIT binary patch literal 393 zcmV;40e1e34Fm}T2=_Vwh@70l%m32Aw*jHYQ|LuUKF3GnNlTz^l)YW~*;Z)7co}$2 z%3`;@xIV9tMyRm$?TCE_U2Jbr?WQlY0+Gwd4z?4xSy6I!*&1IJxyI9%quLvf^yPmw z&6Nz^aup%OA>z?u9cS~c3dTSw9Rs)yYNPvvuUuqD+1JjFaA9!J6x0UGzt5%l>DduF zMx(LR-}2fSk)gog>M|!As?nCA*54vd=z8JlQ_eR(T&@-~ZZQFHg)T<1p2z2S+bZ%- zegb)f)|kaXDfNhkFgZ+mra;5fHfw#!de+a)>2U_KBYrqgW+!Yd`+~Lz;;y%MLHQB4 zwp&;E1jZoD9dG@+SbuIc1TbET-K$ecTvPbaKM4AQIUO6hEUBF!GMf!7OB74V%d3rGoz-oY-gaNvU&%_^aygzb9lt~d>PcyOGJUQct)?jce+JrSb{I}MJXLL0g? z_VPw{&@JYxhyILirXIB}MWJE<3n*+(}&G~^DOSc$YNcYe+-$A%?6Yq!+h*<35b_9`v8#DO0XUDZ5V zSNU~d3uRMQqisMz`*J?^-Az;T5eHmJT+ard$SmUvj=3sj6G;)?8+>uX;^pD_0_a1u z?6r3!a9gIfn<(a_8oA2vrNl1Pn=tBwhv*AXo?ge`|LwFPwAR|c&4wv0vBeW#(0Ruo lHS!L?5kgJ$hi`G%aZwFN@AjN~A+aMh*KBu}DQSb+TY+n+yQKgC diff --git a/envs/notification.env.gpg b/envs/notification.env.gpg index 8383984535bf0e187b109e43f547a8ac0b16d70d..2d35e811e230216336df83d3a063dc6fc79f6162 100644 GIT binary patch literal 194 zcmV;z06qVV4Fm}T2=G#4Iz>?5BmdH|0Wopk@!SWzT-*hJ9qi#HUwtJ2n-exjW^|{W z8e)Tm7zua8t`f!?#hv_B37<1HGk{U#^4RSxNtsT1w_~4t@QywWdiCiRh zI6xf1WZiv*qFG9E?+78Q{jygy!9yzakVS6t-?YM6^37RHI(|;sA$n@=8kb@xFOyP; wwp@E))_#YB16)+CvNu#{m?%ut1en{^THDnL7NfYx_PdWKjoq$XYFOIqfdvI!9smFU literal 183 zcmV;o07(Cg4Fm}T2onepgbu1avH#Mh0qk0�O�з�-%>� -J�+��a����t黓��9����m�dTΧ�+.Y�8��ź�y1��of�x ��1�6�T~�� \ No newline at end of file +�  =:���7 ���p[(S�q!d�Fr���G�+�+R;)@{�C+���F�H��InT;ۈ�p���Re}��]*��h��z�"���+��9�I~Ⱥ����p��B�ڶ��=.�U�]GT� V \ No newline at end of file diff --git a/envs/wishlist-mongo.env.gpg b/envs/wishlist-mongo.env.gpg index 53c61b12e67589866142305fdac0a05c65d3962b..718ebd7ae3c961f9a5fdd0e00866416fd8e6d202 100644 GIT binary patch literal 176 zcmV;h08jsn4Fm}T2#&b3U7xyjF#pn@0o4x6%xf1G_>_X!7ivh9?3|4}$}n{#Pr&#R zKqv*D>TCM0A5M8UHKOpA!u^kxqdi8X=qkxz&Jy0fh@o1ry3M1=NgKC|P+X{Vhoaot z29zdoYHWgTy=ptaO|FPOzIZ9d?sQ~#TC7_(|B3bry}%SxBU`&t716=(0dQJK5+5Zx eZ-S;d4{pvoM*Zvzf(kaPok6xf_wzfC8J*IJW>ql& literal 171 zcmV;c095~s4Fm}T2!;DD8b6$K0RPgO0RlXt*NxUcgcsUgS^tb>uokJr_bzQ^K}rw? zt+F8g5QAAAR4O9Ur!wwGly#bIQvdgHP78&yxVe)R8(2Rl<20D7}{3kV(eBy^uENn$JP^xHu~G0Qd-Z|Bk>zDQ_JuCmO+}~ ZQ_-|1PC=?tJ!J@MCm#n2XXc9|Yi;@=QxyOJ diff --git a/envs/wishlist.env.gpg b/envs/wishlist.env.gpg index de0610b43bc569ed3fe031303ce5906d43a3c406..9bca6c1f6d7fe267d6602e6bb479106df6f0601f 100644 GIT binary patch literal 285 zcmV+&0pk9Q4Fm}T2vcT{43Ex?lHC!AcxuM{d?U_8P zG~2pk1l4fsb@t~0Y?e0{ZW;}C!nbZr8ZF^*e?2&ZsR@`VMSyEd?~&lTY?S^#9CIh5 j?{1u-KRj!9lmme5)OOE*uK0j9DLWz z9srAe**`iH;?NDNC6vOy2N0$o%p-Ht%Fn091rVp-@3ZZuEBw*m^0mn{2QvPcKk6HY zAn2SJ5+ok^RY-hY7cWoSwt}Z5qNVB}7yA>Pte6dylKX{*4gm|S;|Xr*7qlMD;9Yvu g@HO_eIS>)1^E?O&$!xRB52AlzM&@{w`50Vk6X8>kO#lD@ diff --git a/helm/templates/wishlist/service.yaml b/helm/templates/wishlist/service.yaml index b306baa2..e1ec5e01 100644 --- a/helm/templates/wishlist/service.yaml +++ b/helm/templates/wishlist/service.yaml @@ -5,7 +5,7 @@ metadata: spec: type: ClusterIP ports: - - port: 8082 - targetPort: 8082 + - port: 8083 + targetPort: 8083 selector: app: wishlist diff --git a/services/item/Dockerfile b/services/item/Dockerfile index cb48d82f..368b414d 100644 --- a/services/item/Dockerfile +++ b/services/item/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun AS build +FROM oven/bun:alpine AS build WORKDIR /app @@ -10,10 +10,12 @@ COPY . . RUN bun run build -FROM oven/bun AS production +FROM oven/bun:alpine AS production WORKDIR /app +RUN apk add --no-cache curl + COPY --from=build /app/dist/index.js . CMD ["bun", "run", "index.js"] diff --git a/services/item/Dockerfile.dev b/services/item/Dockerfile.dev index 33d22082..3eca31ca 100644 --- a/services/item/Dockerfile.dev +++ b/services/item/Dockerfile.dev @@ -1,7 +1,9 @@ -FROM oven/bun +FROM oven/bun:alpine WORKDIR /app +RUN apk add --no-cache curl + COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile diff --git a/services/notification/Dockerfile b/services/notification/Dockerfile index f160f3ac..e86a954e 100644 --- a/services/notification/Dockerfile +++ b/services/notification/Dockerfile @@ -14,6 +14,8 @@ FROM alpine AS production WORKDIR /app +RUN apk add --no-cache curl + COPY --from=build /app/app . CMD ["./app"] diff --git a/services/notification/main.go b/services/notification/main.go index f523d374..81d6f3a4 100644 --- a/services/notification/main.go +++ b/services/notification/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "net/http" "os" _ "github.com/joho/godotenv/autoload" @@ -34,6 +35,8 @@ func main() { log.Panic(err) } + startHealthCheck() + log.Print("service started") var forever = make(chan struct{}) @@ -80,3 +83,16 @@ func startQueue[Payload interface{ Process() error }](ch *amqp.Channel, topic st return nil } + +func startHealthCheck() { + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + go func() { + err := http.ListenAndServe(":"+os.Getenv("PORT"), nil) + if err != nil { + log.Panic(err) + } + }() +} diff --git a/services/web/Dockerfile b/services/web/Dockerfile index e8f98979..14192dd6 100644 --- a/services/web/Dockerfile +++ b/services/web/Dockerfile @@ -21,6 +21,8 @@ FROM node:lts-alpine AS production WORKDIR /app +RUN apk add --no-cache curl + COPY --from=build /app/.next/standalone . ENV HOSTNAME=0.0.0.0 diff --git a/services/web/Dockerfile.dev b/services/web/Dockerfile.dev index 2172ba74..b4781861 100644 --- a/services/web/Dockerfile.dev +++ b/services/web/Dockerfile.dev @@ -2,7 +2,7 @@ FROM node:lts-alpine WORKDIR /app -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache curl libc6-compat RUN corepack enable pnpm @@ -10,8 +10,6 @@ COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile -COPY . . - ENV HOSTNAME=0.0.0.0 CMD ["pnpm", "run", "dev"] diff --git a/services/web/src/app/healthz/route.ts b/services/web/src/app/healthz/route.ts new file mode 100644 index 00000000..cf09c673 --- /dev/null +++ b/services/web/src/app/healthz/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response("ok"); +} diff --git a/services/wishlist/Dockerfile b/services/wishlist/Dockerfile index fc1f9bf5..b1e1b3ad 100644 --- a/services/wishlist/Dockerfile +++ b/services/wishlist/Dockerfile @@ -1,18 +1,13 @@ +# Stage 1: Build the application FROM maven:3.8-openjdk-17 AS build - WORKDIR /app - COPY pom.xml . COPY src ./src - RUN mvn clean package -DskipTests +# Stage 2: Create a slim production image FROM openjdk:17-jdk-slim - WORKDIR /app - COPY --from=build /app/target/*.jar app.jar - -EXPOSE 8082 - +EXPOSE 8083 ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/services/wishlist/Dockerfile.dev b/services/wishlist/Dockerfile.dev new file mode 100644 index 00000000..0dfcdcfc --- /dev/null +++ b/services/wishlist/Dockerfile.dev @@ -0,0 +1,6 @@ +FROM maven:3.8-openjdk-17 +WORKDIR /app +COPY pom.xml . +COPY src ./src +EXPOSE 8083 5005 +CMD ["mvn", "spring-boot:run", "-Dspring-boot.run.jvmArguments=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"] diff --git a/services/wishlist/Dockerfile.test b/services/wishlist/Dockerfile.test new file mode 100644 index 00000000..0d650af4 --- /dev/null +++ b/services/wishlist/Dockerfile.test @@ -0,0 +1,8 @@ +FROM maven:3.8.5-openjdk-17 AS builder + +WORKDIR /app + +COPY pom.xml ./ +COPY src ./src + +CMD ["mvn", "test", "-DskipClean"] diff --git a/services/wishlist/database/1-schema.js b/services/wishlist/database/1-schema.js index 9b860d0c..026f5248 100644 --- a/services/wishlist/database/1-schema.js +++ b/services/wishlist/database/1-schema.js @@ -4,11 +4,11 @@ db.createCollection("wishlist"); // Create an index to ensure the combination of userId and itemId is unique db.wishlist.createIndex({ userId: 1, itemId: 1 }, { unique: true }); -// Create an index to speed up queries that fetch the latest favorite records for an item, sorted by favoriteDate -db.wishlist.createIndex({ itemId: 1, favoriteDate: -1 }); +// Create an index to speed up queries that fetch the latest favorite records for an item, sorted by wantedAt +db.wishlist.createIndex({ itemId: 1, wantedAt: -1 }); -// Create a single-field index on userId to speed up queries that fetch all favorites of a specific user -db.wishlist.createIndex({ userId: 1 }); +// Create a single-field index on userId to speed up queries that fetch all favorites of a specific user, sorted by wantedAt +db.wishlist.createIndex({ userId: 1, wantedAt: -1 }); // Print confirmation message -print("Wishlist collection initialized with indexes."); +print("Wishlist collection initialized with updated indexes."); diff --git a/services/wishlist/database/dev/1-schema.js b/services/wishlist/database/dev/1-schema.js new file mode 100644 index 00000000..026f5248 --- /dev/null +++ b/services/wishlist/database/dev/1-schema.js @@ -0,0 +1,14 @@ +// Create 'wishlist' collection (if it doesn't exist yet) +db.createCollection("wishlist"); + +// Create an index to ensure the combination of userId and itemId is unique +db.wishlist.createIndex({ userId: 1, itemId: 1 }, { unique: true }); + +// Create an index to speed up queries that fetch the latest favorite records for an item, sorted by wantedAt +db.wishlist.createIndex({ itemId: 1, wantedAt: -1 }); + +// Create a single-field index on userId to speed up queries that fetch all favorites of a specific user, sorted by wantedAt +db.wishlist.createIndex({ userId: 1, wantedAt: -1 }); + +// Print confirmation message +print("Wishlist collection initialized with updated indexes."); diff --git a/services/wishlist/database/dev/2-seed.js b/services/wishlist/database/dev/2-seed.js new file mode 100644 index 00000000..47b9cdb1 --- /dev/null +++ b/services/wishlist/database/dev/2-seed.js @@ -0,0 +1,106 @@ +// Insert seed data into 'wishlist' collection +db.wishlist.insertMany([ + { + _id: ObjectId("670f7703ad0e321f3552e02c"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "581cd614-27a1-4716-889d-e9a22fc27f07", + name: "iPhone 12", + photoUrls: ["https://example.com/iphone12.jpg"], + price: 999.99, + seller: { + _id: "110", + nickname: "Johnny", + avatarUrl: "https://example.com/avatar.png" + }, + status: 1, + type: "single", + userId: 110, + wantedAt: ISODate("2024-10-16T08:19:15.624Z") + }, + { + _id: ObjectId("670f7706ad0e321f3552e02d"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "681cd614-27a1-4716-889d-e9a22fc27f07", + name: "Apple Device Bundle", + price: 1999.99, + seller: { + _id: "seller001", + nickname: "John's Store", + avatarUrl: "http://example.com/avatar.jpg" + }, + status: 1, + type: "pack", + userId: 110, + wantedAt: ISODate("2024-10-16T08:19:18.582Z"), + discount: 10 + }, + { + _id: ObjectId("670f7707ad0e321f3552e02e"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "981cd614-27a1-4716-889d-e9a22fc27f07", + name: "Samsung Galaxy S21", + photoUrls: ["https://example.com/galaxys21.jpg"], + price: 799.99, + seller: { + _id: "seller002", + nickname: "Galaxy Shop", + avatarUrl: "https://example.com/galaxyshop.jpg" + }, + status: 1, + type: "single", + userId: 111, + wantedAt: ISODate("2024-10-17T10:30:45.120Z") + }, + { + _id: ObjectId("670f7708ad0e321f3552e02f"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "781cd614-27a1-4716-889d-e9a22fc27f07", + name: "Smart Home Bundle", + price: 499.99, + seller: { + _id: "seller003", + nickname: "Smart Home Store", + avatarUrl: "https://example.com/smarthome.jpg" + }, + status: 1, + type: "pack", + userId: 111, + wantedAt: ISODate("2024-10-17T11:45:32.524Z"), + discount: 15 + }, + { + _id: ObjectId("670f7709ad0e321f3552e030"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "881cd614-27a1-4716-889d-e9a22fc27f07", + name: "PlayStation 5", + photoUrls: ["https://example.com/ps5.jpg"], + price: 499.99, + seller: { + _id: "seller004", + nickname: "Game World", + avatarUrl: "https://example.com/gameworld.jpg" + }, + status: 1, + type: "single", + userId: 112, + wantedAt: ISODate("2024-10-18T09:25:18.743Z") + }, + { + _id: ObjectId("670f7710ad0e321f3552e031"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "881cd614-27a1-4716-889d-e9a22fc27f07", + name: "Gamer Bundle", + price: 1499.99, + seller: { + _id: "seller005", + nickname: "Gamer's Paradise", + avatarUrl: "https://example.com/gamersparadise.jpg" + }, + status: 1, + type: "pack", + userId: 113, + wantedAt: ISODate("2024-10-18T12:15:47.982Z"), + discount: 20 + } +]); + diff --git a/services/wishlist/database/test/1-schema.js b/services/wishlist/database/test/1-schema.js new file mode 100644 index 00000000..9d254004 --- /dev/null +++ b/services/wishlist/database/test/1-schema.js @@ -0,0 +1,35 @@ +// Switch to the target database +db = db.getSiblingDB('test_nshm_wishlist'); + +// Create 'wishlist' collection (if it doesn't exist yet) +db.createCollection("wishlist"); + +// Create an index to ensure the combination of userId and itemId is unique +db.wishlist.createIndex({ userId: 1, itemId: 1 }, { unique: true }); + +// Create an index to speed up queries that fetch the latest favorite records for an item, sorted by wantedAt +db.wishlist.createIndex({ itemId: 1, wantedAt: -1 }); + +// Create a single-field index on userId to speed up queries that fetch all favorites of a specific user, sorted by wantedAt +db.wishlist.createIndex({ userId: 1, wantedAt: -1 }); + +// Check if the user already exists +var user = db.getUser("test_user"); + +if (!user) { + // Create user with readWrite role on the test_nshm_wishlist database + db.createUser({ + user: "test_user", + pwd: "test_password", // Replace with a secure password in production + roles: [ + { role: "readWrite", db: "test_nshm_wishlist" } + ] + }); + print("User 'test_user' created with readWrite access."); +} else { + print("User 'test_user' already exists."); +} + +// Print confirmation message +print("Wishlist collection initialized with updated indexes."); + diff --git a/services/wishlist/database/test/2-seed.js b/services/wishlist/database/test/2-seed.js new file mode 100644 index 00000000..47b9cdb1 --- /dev/null +++ b/services/wishlist/database/test/2-seed.js @@ -0,0 +1,106 @@ +// Insert seed data into 'wishlist' collection +db.wishlist.insertMany([ + { + _id: ObjectId("670f7703ad0e321f3552e02c"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "581cd614-27a1-4716-889d-e9a22fc27f07", + name: "iPhone 12", + photoUrls: ["https://example.com/iphone12.jpg"], + price: 999.99, + seller: { + _id: "110", + nickname: "Johnny", + avatarUrl: "https://example.com/avatar.png" + }, + status: 1, + type: "single", + userId: 110, + wantedAt: ISODate("2024-10-16T08:19:15.624Z") + }, + { + _id: ObjectId("670f7706ad0e321f3552e02d"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "681cd614-27a1-4716-889d-e9a22fc27f07", + name: "Apple Device Bundle", + price: 1999.99, + seller: { + _id: "seller001", + nickname: "John's Store", + avatarUrl: "http://example.com/avatar.jpg" + }, + status: 1, + type: "pack", + userId: 110, + wantedAt: ISODate("2024-10-16T08:19:18.582Z"), + discount: 10 + }, + { + _id: ObjectId("670f7707ad0e321f3552e02e"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "981cd614-27a1-4716-889d-e9a22fc27f07", + name: "Samsung Galaxy S21", + photoUrls: ["https://example.com/galaxys21.jpg"], + price: 799.99, + seller: { + _id: "seller002", + nickname: "Galaxy Shop", + avatarUrl: "https://example.com/galaxyshop.jpg" + }, + status: 1, + type: "single", + userId: 111, + wantedAt: ISODate("2024-10-17T10:30:45.120Z") + }, + { + _id: ObjectId("670f7708ad0e321f3552e02f"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "781cd614-27a1-4716-889d-e9a22fc27f07", + name: "Smart Home Bundle", + price: 499.99, + seller: { + _id: "seller003", + nickname: "Smart Home Store", + avatarUrl: "https://example.com/smarthome.jpg" + }, + status: 1, + type: "pack", + userId: 111, + wantedAt: ISODate("2024-10-17T11:45:32.524Z"), + discount: 15 + }, + { + _id: ObjectId("670f7709ad0e321f3552e030"), + _class: "edu.nus.market.pojo.SingleLike", + itemId: "881cd614-27a1-4716-889d-e9a22fc27f07", + name: "PlayStation 5", + photoUrls: ["https://example.com/ps5.jpg"], + price: 499.99, + seller: { + _id: "seller004", + nickname: "Game World", + avatarUrl: "https://example.com/gameworld.jpg" + }, + status: 1, + type: "single", + userId: 112, + wantedAt: ISODate("2024-10-18T09:25:18.743Z") + }, + { + _id: ObjectId("670f7710ad0e321f3552e031"), + _class: "edu.nus.market.pojo.PackLike", + itemId: "881cd614-27a1-4716-889d-e9a22fc27f07", + name: "Gamer Bundle", + price: 1499.99, + seller: { + _id: "seller005", + nickname: "Gamer's Paradise", + avatarUrl: "https://example.com/gamersparadise.jpg" + }, + status: 1, + type: "pack", + userId: 113, + wantedAt: ISODate("2024-10-18T12:15:47.982Z"), + discount: 20 + } +]); + diff --git a/services/wishlist/docker-compose.test.yaml b/services/wishlist/docker-compose.test.yaml new file mode 100644 index 00000000..4b806f03 --- /dev/null +++ b/services/wishlist/docker-compose.test.yaml @@ -0,0 +1,28 @@ +services: + api: + build: + context: . + dockerfile: Dockerfile.test + environment: + - MONGO_HOST=mongo + - MONGO_PORT=27017 + - MONGO_DB=test_nshm_wishlist + - MONGO_USERNAME=test_user + - MONGO_PASSWORD=test_password + - JWT_SECRET_KEY=test_jwt_secret_key + - WISHLIST_PORT=8083 + depends_on: + - mongo + volumes: + - ./reports:/app/target/site/jacoco + + mongo: + image: mongo + environment: + - MONGO_INITDB_DATABASE=test_nshm_wishlist + - MONGO_INITDB_ROOT_USERNAME=test_user + - MONGO_INITDB_ROOT_PASSWORD=test_password + ports: + - 27017:27017 + volumes: + - ./database/test:/docker-entrypoint-initdb.d diff --git a/services/wishlist/pom.xml b/services/wishlist/pom.xml index 9ed83181..d7d01c09 100644 --- a/services/wishlist/pom.xml +++ b/services/wishlist/pom.xml @@ -2,34 +2,26 @@ 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.3.4 - + + edu.nus wishlist 0.0.1-SNAPSHOT wishlist wishlist - - - - - - - - - - - - - + 17 + + org.springframework.boot spring-boot-starter-data-mongodb @@ -46,26 +38,12 @@ org.springframework.boot spring-boot-starter-web - - - org.projectlombok - lombok - true - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - jakarta.validation - jakarta.validation-api + spring-boot-starter-validation + + io.jsonwebtoken jjwt-api @@ -78,9 +56,30 @@ io.jsonwebtoken - jjwt-jackson + jjwt-jackson 0.11.2 + + + + org.projectlombok + lombok + true + + + io.github.cdimascio + java-dotenv + 5.2.2 + + + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot spring-boot-starter-test @@ -92,19 +91,13 @@ test - org.testcontainers - junit-jupiter + org.springframework.security + spring-security-test test - org.springframework.boot - spring-boot-starter-validation - - - - org.springframework.boot - - spring-boot-testcontainers + org.testcontainers + junit-jupiter test @@ -112,11 +105,17 @@ mongodb test - + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.2.Final + compile + + org.springframework.boot spring-boot-maven-plugin @@ -129,7 +128,40 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + ${project.build.directory}/surefire-reports + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + + diff --git a/services/wishlist/src/main/java/edu/nus/market/controller/WishlistController.java b/services/wishlist/src/main/java/edu/nus/market/controller/WishlistController.java index 7a9d5dfa..967a2302 100644 --- a/services/wishlist/src/main/java/edu/nus/market/controller/WishlistController.java +++ b/services/wishlist/src/main/java/edu/nus/market/controller/WishlistController.java @@ -1,7 +1,5 @@ package edu.nus.market.controller; - - import edu.nus.market.pojo.*; import edu.nus.market.pojo.ReqEntity.AddLikeReq; @@ -15,8 +13,12 @@ import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Objects; + @RestController @RequestMapping("/wishlists") @@ -35,7 +37,8 @@ public String helloWorld(){ // Register and Delete @GetMapping("/{user_id}") - public ResponseEntity getWishlist(@PathVariable("user_id") int userId, @RequestHeader(value = "Cookie", required = false) String token){ + public ResponseEntity getWishlist(@PathVariable("user_id") int userId, @RequestHeader(value = "Cookie", required = false) String token, + @RequestParam(value = "before", required = false) String beforeString) { if (token == null || token.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorMsg(ErrorMsgEnum.NOT_LOGGED_IN.ErrorMsg)); } @@ -45,7 +48,18 @@ public ResponseEntity getWishlist(@PathVariable("user_id") int userId, @ if (userId != JwtTokenManager.decodeCookie(token).getId()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorMsg(ErrorMsgEnum.UNAUTHORIZED_ACCESS.ErrorMsg)); } - return wishlistService.getWishlistService(userId); + Date before = new Date(); + if (beforeString != null && !beforeString.isEmpty()) { + try{ + beforeString = beforeString.replace(" ", "+"); + before = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").parse(beforeString); + } + catch (ParseException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorMsg(ErrorMsgEnum.INVALID_DATA.ErrorMsg)); + } + } + + return wishlistService.getWishlistService(userId, before); } @PostMapping("/{user_id}/items/{item_id}") @@ -90,8 +104,8 @@ public ResponseEntity deleteLike(@PathVariable("user_id") int userId, @P return wishlistService.deleteLikeService(userId, itemId); } - @GetMapping("/statistics/{item_id}") + @GetMapping("/statistics/{item_id}") public ResponseEntity getItemLikeInfo(@PathVariable("item_id") String itemId, @RequestHeader(value = "Cookie", required = false) String token){ // account verification if (token == null || token.isEmpty()) diff --git a/services/wishlist/src/main/java/edu/nus/market/converter/ConvertDateToISO.java b/services/wishlist/src/main/java/edu/nus/market/converter/ConvertDateToISO.java new file mode 100644 index 00000000..8a36bd21 --- /dev/null +++ b/services/wishlist/src/main/java/edu/nus/market/converter/ConvertDateToISO.java @@ -0,0 +1,11 @@ +package edu.nus.market.converter; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ConvertDateToISO { + public static String convert(Date date) { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + return isoFormat.format(date); + } + +} diff --git a/services/wishlist/src/main/java/edu/nus/market/dao/WishlistDao.java b/services/wishlist/src/main/java/edu/nus/market/dao/WishlistDao.java index 08d91838..1bdf74e5 100644 --- a/services/wishlist/src/main/java/edu/nus/market/dao/WishlistDao.java +++ b/services/wishlist/src/main/java/edu/nus/market/dao/WishlistDao.java @@ -13,8 +13,7 @@ public interface WishlistDao extends MongoRepository { // 自定义查询方法 - List findByUserIdOrderByWantedAtDesc(int userId); - + List findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(int userId, Date before); //insert one Like just use save() diff --git a/services/wishlist/src/main/java/edu/nus/market/pojo/Account.java b/services/wishlist/src/main/java/edu/nus/market/pojo/Account.java new file mode 100644 index 00000000..9c83a024 --- /dev/null +++ b/services/wishlist/src/main/java/edu/nus/market/pojo/Account.java @@ -0,0 +1,53 @@ +package edu.nus.market.pojo; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Account { + @Column(name = "id") + int id; + + @Column(name = "email") + String email; + + @Column(name = "nickname") + String nickname; + + @Column(name = "password_hash") + String passwordHash; + + @Column(name = "password_salt") + String passwordSalt; + + @Column(name = "avatar_url") + String avatarUrl; + + @Column(name = "department_id") + int departmentId; + + @Column(name = "phone_code") + String phoneCode; + + @Column(name = "phone_number") + String phoneNumber; + + @Column(name = "preferred_currency") + String preferredCurrency; + + @Column(name = "created_at") + String createdAt; + + @Column(name = "deleted_at") + String deletedAt; + + + +} diff --git a/services/wishlist/src/main/java/edu/nus/market/pojo/ReqEntity/AddLikeReq.java b/services/wishlist/src/main/java/edu/nus/market/pojo/ReqEntity/AddLikeReq.java index 38f0d128..c7f4c563 100644 --- a/services/wishlist/src/main/java/edu/nus/market/pojo/ReqEntity/AddLikeReq.java +++ b/services/wishlist/src/main/java/edu/nus/market/pojo/ReqEntity/AddLikeReq.java @@ -39,6 +39,7 @@ public class AddLikeReq { @NotNull private int status; + // for SINGLE Item private String[] photoUrls; @@ -49,4 +50,5 @@ public class AddLikeReq { + } diff --git a/services/wishlist/src/main/java/edu/nus/market/pojo/ResEntity/JWTPayload.java b/services/wishlist/src/main/java/edu/nus/market/pojo/ResEntity/JWTPayload.java new file mode 100644 index 00000000..1a77380d --- /dev/null +++ b/services/wishlist/src/main/java/edu/nus/market/pojo/ResEntity/JWTPayload.java @@ -0,0 +1,24 @@ +package edu.nus.market.pojo.ResEntity; + +import edu.nus.market.pojo.Account; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JWTPayload { + int id; + + String nickname; + + String avatarUrl; + + // all args constructor, input an account object, and convert to this rspAccount object + public JWTPayload(Account account) { + this.id = account.getId(); + this.nickname = account.getNickname(); + this.avatarUrl = account.getAvatarUrl(); + } +} diff --git a/services/wishlist/src/main/java/edu/nus/market/security/JwtTokenManager.java b/services/wishlist/src/main/java/edu/nus/market/security/JwtTokenManager.java index bab699e0..95474067 100644 --- a/services/wishlist/src/main/java/edu/nus/market/security/JwtTokenManager.java +++ b/services/wishlist/src/main/java/edu/nus/market/security/JwtTokenManager.java @@ -1,36 +1,45 @@ package edu.nus.market.security; -import edu.nus.market.pojo.ResEntity.ResAccount; +import edu.nus.market.pojo.ResEntity.JWTPayload; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.annotation.PostConstruct; +import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.net.HttpCookie; import java.security.SecureRandom; +import java.util.Arrays; import java.util.Base64; import java.util.Date; +import java.util.List; @Component public class JwtTokenManager { + //set secret key + @Setter private static String secretKey; private static final long expirationTime = 7 * 24 * 60 * 60 * 1000; // 1 week in milliseconds - public static String generateAccessToken(ResAccount resAccount){ + @Value("${jwt.secretKey}") + private String injectedSecretKey; + + @PostConstruct + public void init() { + JwtTokenManager.secretKey = injectedSecretKey; + } + + public static String generateAccessToken(JWTPayload jwtPayload){ return Jwts.builder() - .setSubject(String.valueOf(resAccount.getId())) - .claim("email", resAccount.getEmail()) - .claim("nickname", resAccount.getNickname()) - .claim("avatarUrl", resAccount.getAvatarUrl()) - .claim("departmentId", resAccount.getDepartmentId()) - .claim("phoneCode", resAccount.getPhoneCode()) - .claim("phoneNumber", resAccount.getPhoneNumber()) - .claim("preferredCurrency", resAccount.getPreferredCurrency()) - .claim("createdAt", resAccount.getCreatedAt()) - .claim("deletedAt", resAccount.getDeletedAt()) - .setIssuedAt(new Date())//登录时间 + .claim("id", jwtPayload.getId()) + .claim("nickname", jwtPayload.getNickname()) + .claim("avatar_url", jwtPayload.getAvatarUrl()) + + .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256, secretKey) .setExpiration(new Date(new Date().getTime() + expirationTime)) .compact(); @@ -47,12 +56,20 @@ public static boolean validateToken(String token) { public static boolean validateCookie(String Cookie) { // extract the access token from the cookie and validate it using validateToken method - String token = Cookie.split("; ")[0].split("=")[1]; + String token = Arrays.stream(Cookie.split("; ")) + .filter(part -> part.startsWith("access_token=")) + .map(part -> part.split("=")[1]) + .findFirst() + .orElse(null); + + if (token == null) { + return false; + } return validateToken(token); } - public static ResAccount decodeAccessToken(String token) { + public static JWTPayload decodeAccessToken(String token) { try { // decode JWT Claims claims = Jwts.parser() @@ -60,19 +77,29 @@ public static ResAccount decodeAccessToken(String token) { .parseClaimsJws(token) .getBody(); - ResAccount resAccount = new ResAccount(Integer.parseInt(claims.getSubject()), (String)claims.get("email"), (String)claims.get("nickname"), - (String)claims.get("avatarUrl"), (int)claims.get("departmentId"), (String)claims.get("phoneCode"), (String)claims.get("phoneNumber"), - (String)claims.get("preferredCurrency"), (String)claims.get("createdAt"), (String)claims.get("deletedAt")); + JWTPayload jwtPayload = new JWTPayload((int) claims.get("id"), (String)claims.get("nickname"), + (String)claims.get("avatar_url")); - return resAccount; + return jwtPayload; } catch (Exception e) { throw new RuntimeException("Token decoding failed", e); } } - public static ResAccount decodeCookie(String cookie) { - String token = cookie.split("; ")[0].split("=")[1]; + public static JWTPayload decodeCookie(String cookie) { + if (cookie == null || cookie.isEmpty()) { + throw new IllegalArgumentException("Cookie cannot be null or empty"); + } + + List cookies = HttpCookie.parse(cookie); + + String token = cookies.stream() + .filter(c -> "access_token".equals(c.getName())) + .map(HttpCookie::getValue) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Missing access_token in the provided cookie")); + return decodeAccessToken(token); } @@ -83,12 +110,4 @@ public static String generateSecretKey(){ return Base64.getEncoder().encodeToString(randomBytes); } - // set secret key - public static void setSecretKey(String secretKey){ - JwtTokenManager.secretKey = secretKey; - } - - public JwtTokenManager(@Value("${jwt.secretKey}")String secretKey){ - this.secretKey = secretKey; - } } diff --git a/services/wishlist/src/main/java/edu/nus/market/service/WishlistService.java b/services/wishlist/src/main/java/edu/nus/market/service/WishlistService.java index 9d0c619e..e99a174f 100644 --- a/services/wishlist/src/main/java/edu/nus/market/service/WishlistService.java +++ b/services/wishlist/src/main/java/edu/nus/market/service/WishlistService.java @@ -3,8 +3,10 @@ import edu.nus.market.pojo.ReqEntity.AddLikeReq; import org.springframework.http.ResponseEntity; +import java.util.Date; + public interface WishlistService { - ResponseEntity getWishlistService(int userId); + ResponseEntity getWishlistService(int userId, Date before); ResponseEntity addLikeService(AddLikeReq addLikeReq); diff --git a/services/wishlist/src/main/java/edu/nus/market/service/WishlistServiceImpl.java b/services/wishlist/src/main/java/edu/nus/market/service/WishlistServiceImpl.java index b0339d2b..a2f5644d 100644 --- a/services/wishlist/src/main/java/edu/nus/market/service/WishlistServiceImpl.java +++ b/services/wishlist/src/main/java/edu/nus/market/service/WishlistServiceImpl.java @@ -1,5 +1,6 @@ package edu.nus.market.service; +import edu.nus.market.converter.ConvertDateToISO; import edu.nus.market.dao.WishlistDao; import edu.nus.market.pojo.ReqEntity.AddLikeReq; import edu.nus.market.pojo.ErrorMsg; @@ -9,6 +10,7 @@ import edu.nus.market.pojo.ResEntity.ResItemLikeInfo; import jakarta.annotation.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -24,10 +26,22 @@ public class WishlistServiceImpl implements WishlistService { private WishlistDao wishlistDao; + + @Override - public ResponseEntity getWishlistService(int id) { - List likes = wishlistDao.findByUserIdOrderByWantedAtDesc(id); - return ResponseEntity.status(HttpStatus.OK).body(likes); + public ResponseEntity getWishlistService(int id, Date before) { + List likes = wishlistDao.findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(id, before); + //find the nextBefore date + Date nextBefore = before; + HttpHeaders headers = new HttpHeaders(); + if (!likes.isEmpty()) { + nextBefore = likes.get(likes.size() - 1).getWantedAt(); + } + headers.add("Next-Before", ConvertDateToISO.convert(nextBefore)); + + + + return ResponseEntity.ok().headers(headers).body(likes); } @Override diff --git a/services/wishlist/src/main/resources/application.yml b/services/wishlist/src/main/resources/application.yml index 15e6d5b4..fbb97b70 100644 --- a/services/wishlist/src/main/resources/application.yml +++ b/services/wishlist/src/main/resources/application.yml @@ -1,10 +1,8 @@ -server: - port: ${PORT} - spring: application: name: wishlist - + config: + import: "optional:file:.env" jackson: property-naming-strategy: SNAKE_CASE @@ -16,5 +14,9 @@ spring: username: ${MONGO_USERNAME} password: ${MONGO_PASSWORD} +server: + port: ${WISHLIST_PORT} + + jwt: secretKey: ${JWT_SECRET_KEY} diff --git a/services/wishlist/src/test/java/edu/nus/market/WishlistControllerTest.java b/services/wishlist/src/test/java/edu/nus/market/WishlistControllerTest.java index d9f0f420..b0909979 100644 --- a/services/wishlist/src/test/java/edu/nus/market/WishlistControllerTest.java +++ b/services/wishlist/src/test/java/edu/nus/market/WishlistControllerTest.java @@ -1,7 +1,9 @@ package edu.nus.market; import edu.nus.market.controller.WishlistController; +import edu.nus.market.converter.ConvertDateToISO; import edu.nus.market.pojo.ReqEntity.AddLikeReq; +import edu.nus.market.pojo.ResEntity.JWTPayload; import edu.nus.market.pojo.ResEntity.ResAccount; import edu.nus.market.pojo.*; import edu.nus.market.security.CookieManager; @@ -17,6 +19,7 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.MapBindingResult; +import java.text.ParseException; import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -42,7 +45,7 @@ class WishlistControllerTest { void setUp() { MockitoAnnotations.openMocks(this); prepareTestData(); - JwtTokenManager.setSecretKey("9lRZUYgnElr2PnI9K/yAxIyX+kR31vGRCuGFfRs5ZVE="); // 设置静态密钥 + JwtTokenManager.setSecretKey(JwtTokenManager.generateSecretKey()); // 设置静态密钥 generateValidToken(); } @@ -62,16 +65,14 @@ private void prepareTestData() { } private void generateValidToken() { - ResAccount resAccount = new ResAccount( - 1, "user@example.com", "testuser", "http://example.com/avatar.jpg", - 123, "+65", "12345678", "SGD", "2024-01-01", null - ); - cookie = cookieManager.generateCookie(JwtTokenManager.generateAccessToken(resAccount)).toString(); + JWTPayload jwtPayload = new JWTPayload( + 1, "testuser", "http://example.com/avatar.jpg"); + cookie = cookieManager.generateCookie(JwtTokenManager.generateAccessToken(jwtPayload)).toString(); } @Test void testGetWishlist_Unauthorized() { - ResponseEntity response = wishlistController.getWishlist(1, ""); + ResponseEntity response = wishlistController.getWishlist(1, "", ConvertDateToISO.convert(new Date())); assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); } @@ -110,9 +111,9 @@ void testAddLike_Unauthorized() { @Test void testGetWishlist_Success() { - when(wishlistService.getWishlistService(anyInt())).thenReturn(ResponseEntity.ok(mockLikes)); + when(wishlistService.getWishlistService(anyInt(),any())).thenReturn(ResponseEntity.ok(mockLikes)); - ResponseEntity response = wishlistController.getWishlist(1, cookie); + ResponseEntity response = wishlistController.getWishlist(1, cookie, ConvertDateToISO.convert(new Date())); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(mockLikes, response.getBody()); diff --git a/services/wishlist/src/test/java/edu/nus/market/WishlistDAOTest.java b/services/wishlist/src/test/java/edu/nus/market/WishlistDAOTest.java index eb71c121..f6ddad93 100644 --- a/services/wishlist/src/test/java/edu/nus/market/WishlistDAOTest.java +++ b/services/wishlist/src/test/java/edu/nus/market/WishlistDAOTest.java @@ -41,7 +41,8 @@ public void testAddAndRetrieveLike_Success() { wishlistDao.save(like); // Act: 读取该用户的收藏 - List likes = wishlistDao.findByUserIdOrderByWantedAtDesc(1); + List likes = wishlistDao.findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(1,new Date()); + // Assert: 验证添加和读取成功 assertEquals(1, likes.size()); assertEquals(ITEM_ID, likes.get(0).getItemId()); @@ -114,7 +115,7 @@ public void testConcurrentWritesAndReads() throws InterruptedException, Executio // Act: 验证每个用户的收藏读取是否隔离 for (int i = 1; i <= USER_COUNT; i++) { final int userId = i; - List likes = wishlistDao.findByUserIdOrderByWantedAtDesc(userId); + List likes = wishlistDao.findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(userId, new Date()); assertEquals(1, likes.size()); assertEquals(userId, likes.get(0).getUserId()); } diff --git a/services/wishlist/src/test/java/edu/nus/market/WishlistServiceImplTest.java b/services/wishlist/src/test/java/edu/nus/market/WishlistServiceImplTest.java index 9b916c5e..977f1284 100644 --- a/services/wishlist/src/test/java/edu/nus/market/WishlistServiceImplTest.java +++ b/services/wishlist/src/test/java/edu/nus/market/WishlistServiceImplTest.java @@ -40,6 +40,7 @@ void setUp() { private void prepareTestData() { mockLike = new SingleLike(); mockLike.setUserId(1); + mockLike.setItemId("item001"); mockLike.setName("iPhone 12"); mockLike.setStatus(1); mockLike.setWantedAt(new Date()); @@ -48,9 +49,9 @@ private void prepareTestData() { @Test void testGetWishlistService_Success() { List mockLikes = List.of(mockLike); - when(wishlistDao.findByUserIdOrderByWantedAtDesc(anyInt())).thenReturn(mockLikes); + when(wishlistDao.findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(anyInt(),any())).thenReturn(mockLikes); - ResponseEntity response = wishlistService.getWishlistService(1); + ResponseEntity response = wishlistService.getWishlistService(1, new Date()); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(mockLikes, response.getBody()); @@ -58,8 +59,9 @@ void testGetWishlistService_Success() { @Test void testGetWishlistService_EmptyList() { - when(wishlistDao.findByUserIdOrderByWantedAtDesc(anyInt())).thenReturn(List.of()); - ResponseEntity response = wishlistService.getWishlistService(1); + when(wishlistDao.findTop10ByUserIdAndWantedAtBeforeOrderByWantedAtDesc(anyInt(),any())).thenReturn(List.of()); + + ResponseEntity response = wishlistService.getWishlistService(1, new Date()); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(0, ((List) response.getBody()).size());