diff --git a/app/front/apiClient/index.ts b/app/front/apiClient/index.ts index 94892b28..d4c82042 100644 --- a/app/front/apiClient/index.ts +++ b/app/front/apiClient/index.ts @@ -1,5 +1,6 @@ import { NuxtAxiosInstance } from "@nuxtjs/axios" -import { RestApi, PathsWithMethod } from "sushi-chat-shared" +import { RestApi, PathsWithMethod, EmptyRecord } from "sushi-chat-shared" +import getIdToken from "~/utils/getIdToken" type PathObject< Method extends "get" | "post" | "put", @@ -9,6 +10,13 @@ type PathObject< params: RestApi["params"] } +type PathOrPathObj< + Method extends "get" | "post" | "put", + Path extends PathsWithMethod, +> = Path extends `${string}:${string}` + ? PathObject + : Path | PathObject + /** * PathObjectからパスを組み立てる関数 * @param path PathObject @@ -32,7 +40,6 @@ const pathBuilder = (path: { * async asyncData({ app }) { * const sampleResponse = await app.$apiClient.get( * { pathname: "/room/:id/history", params: { id: "roomId" } }, - * {}, * ) * if (sampleResponse.result === "success") { * const rooms = sampleResponse.data @@ -54,7 +61,8 @@ export default class Repository { * idTokenを設定する * @param idToken idToken */ - public setToken(idToken: string | null) { + public async setToken() { + const idToken = await getIdToken() if (idToken != null) { this.nuxtAxios.setToken(`Bearer ${idToken}`) } else { @@ -65,18 +73,18 @@ export default class Repository { /** * getリクエストを行う * @param path エンドポイントを表すPathObjectまたはパス文字列(パスパラメータ(`:xyz`)を含まない場合は直接文字列を指定可能) - * @param data 送信するデータ + * @param query クエリパラメータ * @returns レスポンス */ public async get>( - path: Path extends `${string}:${string}` - ? PathObject<"get", Path> - : Path | PathObject<"get", Path>, - data: RestApi<"get", Path>["request"], + ...[path, query]: RestApi<"get", Path>["query"] extends EmptyRecord + ? [path: PathOrPathObj<"get", Path>] + : [path: PathOrPathObj<"get", Path>, query: RestApi<"get", Path>["query"]] ) { + await this.setToken() return await this.nuxtAxios.$get["response"]>( typeof path === "string" ? path : pathBuilder(path), - { data }, + { params: query }, ) } @@ -87,14 +95,32 @@ export default class Repository { * @returns レスポンス */ public async post>( - path: Path extends `${string}:${string}` - ? PathObject<"post", Path> - : Path | PathObject<"post", Path>, - data: RestApi<"post", Path>["request"], + ...[path, body, query]: RestApi<"post", Path>["request"] extends EmptyRecord + ? RestApi<"post", Path>["query"] extends EmptyRecord + ? [path: PathOrPathObj<"post", Path>] + : [ + path: PathOrPathObj<"post", Path>, + body: RestApi<"post", Path>["request"], + query: RestApi<"post", Path>["query"], + ] + : RestApi<"post", Path>["query"] extends EmptyRecord + ? [ + path: PathOrPathObj<"post", Path>, + body: RestApi<"post", Path>["request"], + ] + : [ + path: PathOrPathObj<"post", Path>, + body: RestApi<"post", Path>["request"], + query: RestApi<"post", Path>["query"], + ] ) { + await this.setToken() return await this.nuxtAxios.$post["response"]>( typeof path === "string" ? path : pathBuilder(path), - data, + body, + { + params: query, + }, ) } @@ -105,14 +131,32 @@ export default class Repository { * @returns レスポンス */ public async put>( - path: Path extends `${string}:${string}` - ? PathObject<"put", Path> - : Path | PathObject<"put", Path>, - data: RestApi<"put", Path>["request"], + ...[path, data, query]: RestApi<"put", Path>["request"] extends EmptyRecord + ? RestApi<"put", Path>["query"] extends EmptyRecord + ? [path: PathOrPathObj<"put", Path>] + : [ + path: PathOrPathObj<"put", Path>, + body: RestApi<"put", Path>["request"], + query: RestApi<"put", Path>["query"], + ] + : RestApi<"put", Path>["query"] extends EmptyRecord + ? [ + path: PathOrPathObj<"put", Path>, + body: RestApi<"put", Path>["request"], + ] + : [ + path: PathOrPathObj<"put", Path>, + body: RestApi<"put", Path>["request"], + query: RestApi<"put", Path>["query"], + ] ) { + await this.setToken() return await this.nuxtAxios.$put["response"]>( typeof path === "string" ? path : pathBuilder(path), data, + { + params: query, + }, ) } } diff --git a/app/front/assets/scss/home/home.scss b/app/front/assets/scss/home/home.scss index 5f16c3f3..c9300eee 100644 --- a/app/front/assets/scss/home/home.scss +++ b/app/front/assets/scss/home/home.scss @@ -126,12 +126,13 @@ display: grid; margin: 1rem 0; grid-template-columns: auto 1fr; + align-items: center; &__index { grid-column: 1; margin: 0 0.5rem 0 0; &--element { - margin: 0.2rem 0 0.9em 0; - padding: 0.5rem 0; + margin: 0 0 0.5rem 0; + padding: 0.55rem 0; } } &__list { @@ -139,15 +140,14 @@ &--element { display: flex; align-items: center; - width: 85%; margin: 0 0 0.5rem 0; &--input { display: flex; flex: 1; margin: 0 0.5rem 0 0; padding: 0 !important; - @include white-text-box($expand: "true"); input { + width: 100%; border: none; flex: 1; padding: 0; @@ -158,7 +158,7 @@ } } &--remove { - margin: 0.2rem; + margin: 0.2rem 0; @include material-icon-button( $size: "sm", $color: #f07b7b, @@ -171,6 +171,7 @@ } } &--sort { + margin: 0; cursor: grab !important; @include material-icon-button; &--dragging { diff --git a/app/front/assets/scss/home/variables.scss b/app/front/assets/scss/home/variables.scss index f5fa2555..e26d95c2 100644 --- a/app/front/assets/scss/home/variables.scss +++ b/app/front/assets/scss/home/variables.scss @@ -88,8 +88,8 @@ $color: $text-gray, $border: "none" ) { - width: 36px; - height: 36px; + width: 24px; + height: 24px; display: inline-flex; align-items: center; justify-content: center; diff --git a/app/front/assets/scss/index.scss b/app/front/assets/scss/index.scss index 3356afe0..75105627 100644 --- a/app/front/assets/scss/index.scss +++ b/app/front/assets/scss/index.scss @@ -105,7 +105,7 @@ &__details { position: relative; z-index: 0; - background-color: white; + background-color: rgba(255, 255, 255, 0.1); padding: 15px; border-bottom: 1px solid rgb(239, 239, 239); &--filter-btn { @@ -150,11 +150,20 @@ & > .icon { transform: rotate(20deg); } - &--text { + &--button { flex: 1; - font-size: 14px; + } + &--content { + display: -webkit-box; word-break: break-all; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; text-align: left; + font-size: 100%; + width: 100%; + max-height: 2.5rem; } &--close-icon { flex-shrink: 0; @@ -228,7 +237,6 @@ overflow-y: hidden; & > .scrollable { - scroll-snap-type: y mandatory; overflow-y: scroll; padding: 1em 1rem; @include scrollbar-hidden; @@ -628,7 +636,6 @@ .reaction { position: relative; width: 100%; - min-height: 5rem; flex-grow: 1; flex-shrink: 0; padding: 0em 0.8em 0.35em; @@ -655,26 +662,25 @@ word-break: break-all; overflow-wrap: anywhere; margin-left: 0.5em; - & > .long-text { + & .long-text--button { flex: 1; - display: -webkit-box; + } + & .long-text--content { color: $text-gray; font-size: 80%; text-align: left; + display: -webkit-box; word-break: break-all; overflow-wrap: anywhere; margin-bottom: 0.3rem; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; - &:hover { - text-decoration: underline; - cursor: pointer; - } + width: 100%; + max-height: 2rem; } } } - .comment-footer { display: flex; align-items: flex-end; diff --git a/app/front/assets/scss/sushiselect.scss b/app/front/assets/scss/sushiselect.scss index 6f3675d7..0cc64402 100644 --- a/app/front/assets/scss/sushiselect.scss +++ b/app/front/assets/scss/sushiselect.scss @@ -90,7 +90,8 @@ } &--speaker { position: relative; - width: 400px; + width: 100%; + max-width: 400px; margin-bottom: 50px; & > .select-speaker { -webkit-appearance: none; @@ -99,15 +100,12 @@ width: 100%; padding: 10px; padding-right: 2.5em; - border: 1px solid transparent; - border-radius: 10px; + border: none; + border-radius: 4px; + background: white; outline: none; cursor: pointer; @include text-size("normal"); - - &:focus-visible { - border-color: rgb(214, 214, 214); - } } & > .select-icon { diff --git a/app/front/components/ChatRoom.vue b/app/front/components/ChatRoom.vue index e1c7526f..07bcc661 100644 --- a/app/front/components/ChatRoom.vue +++ b/app/front/components/ChatRoom.vue @@ -37,8 +37,10 @@
-
@@ -59,7 +61,11 @@ :topic-id="topicId" />
- @@ -197,7 +203,7 @@ export default Vue.extend({ } catch (e) { window.alert("メッセージの送信に失敗しました") } - this.clickScroll() + this.scrollToBottom() this.selectedChatItem = null }, // リアクションボタン @@ -236,8 +242,9 @@ export default Vue.extend({ } }, // いちばん下までスクロール - clickScroll() { + scrollToBottom() { const element: Element | null = (this.$refs.scrollable as Vue).$el + console.log(element?.scrollHeight) if (element) { element.scrollTo({ top: element.scrollHeight, @@ -275,6 +282,9 @@ export default Vue.extend({ }, clickShowAll() { this.isAllCommentShowed = true + Vue.nextTick(() => { + setTimeout(() => this.scrollToBottom(), 100) + }) }, clickNotShowAll() { this.isAllCommentShowed = false diff --git a/app/front/components/Home/Modal.vue b/app/front/components/Home/Modal.vue index bf9c5010..3d29c414 100644 --- a/app/front/components/Home/Modal.vue +++ b/app/front/components/Home/Modal.vue @@ -40,7 +40,7 @@ export default Vue.extend({ computed: { width() { if (DeviceStore.device === "smartphone") { - return "100%" + return "90%" } else { return "50%" } diff --git a/app/front/components/Message.vue b/app/front/components/Message.vue index 2bf027ef..d74e5308 100644 --- a/app/front/components/Message.vue +++ b/app/front/components/Message.vue @@ -35,16 +35,18 @@
diff --git a/app/front/components/TopicHeader.vue b/app/front/components/TopicHeader.vue index 0a3f2b1e..0da58ac9 100644 --- a/app/front/components/TopicHeader.vue +++ b/app/front/components/TopicHeader.vue @@ -11,11 +11,12 @@
{{ title }}
@@ -51,19 +52,22 @@ >
diff --git a/app/front/middleware/privateRoute.ts b/app/front/middleware/privateRoute.ts index 11e707eb..7b9600bc 100644 --- a/app/front/middleware/privateRoute.ts +++ b/app/front/middleware/privateRoute.ts @@ -1,7 +1,8 @@ import { Middleware } from "@nuxt/types" +import { AuthStore } from "~/store" -const privateRoute: Middleware = ({ store, redirect }) => { - if (!store.getters["auth/isLoggedIn"]) { +const privateRoute: Middleware = ({ redirect }) => { + if (!AuthStore.isLoggedIn) { return redirect("/login") } } diff --git a/app/front/pages/home.vue b/app/front/pages/home.vue index 18bf31e8..e5b70e60 100644 --- a/app/front/pages/home.vue +++ b/app/front/pages/home.vue @@ -126,7 +126,8 @@ export default Vue.extend({ layout: "home", middleware: "privateRoute", async asyncData({ app }): Promise { - const response = await app.$apiClient.get("/room", {}) + const response = await app.$apiClient.get("/room") + if (response.result === "success") { const rooms = response.data const ongoingRooms = rooms.filter((room) => room.state === "ongoing") @@ -174,13 +175,10 @@ export default Vue.extend({ } try { console.log(id) - const response = await this.$apiClient.put( - { - pathname: "/room/:id/archive", - params: { id }, - }, - {}, - ) + const response = await this.$apiClient.put({ + pathname: "/room/:id/archive", + params: { id }, + }) if (response.result === "success") { this.finishedRooms = this.finishedRooms.filter( diff --git a/app/front/pages/index.vue b/app/front/pages/index.vue index b967fcbe..9a67d883 100644 --- a/app/front/pages/index.vue +++ b/app/front/pages/index.vue @@ -55,6 +55,7 @@ import { StampModel, PubUserCountParam, PubPinnedMessageParam, + EnterRoomResponse, } from "sushi-chat-shared" import AdminTool from "@/components/AdminTool/AdminTool.vue" import ChatRoom from "@/components/ChatRoom.vue" @@ -102,7 +103,7 @@ export default Vue.extend({ }, computed: { isRoomStarted(): boolean { - return this.room.state === "ongoing" + return this.room.state === "ongoing" || this.room.state === "finished" }, isAdmin(): boolean { return UserItemStore.userItems.isAdmin @@ -138,20 +139,18 @@ export default Vue.extend({ this.checkStatusAndAction() DeviceStore.determineOs() }, - beforeDestroy() { - this.$socket()?.disconnect() + async beforeDestroy() { + const socket = await this.$socket() + socket.disconnect() }, methods: { async checkStatusAndAction() { // ルーム情報取得・status更新 const res = await this.$apiClient - .get( - { - pathname: "/room/:id", - params: { id: this.room.id }, - }, - {}, - ) + .get({ + pathname: "/room/:id", + params: { id: this.room.id }, + }) .catch((e) => { throw new Error(e) }) @@ -163,47 +162,71 @@ export default Vue.extend({ this.roomState = res.data.state TopicStore.set(res.data.topics) - // 開催中の時 if (this.room.state === "ongoing") { + // 開催中 if (this.isAdmin) { this.adminEnterRoom() } else { // ユーザーの入室 this.$modal.show("sushi-modal") } + } else if (this.room.state === "finished") { + // 終了時。すべてのトピックが閉じたルームのアーカイブが閲覧できる。 + const history = await this.$apiClient + .get({ + pathname: "/room/:id/history", + params: { id: this.room.id }, + }) + .catch((e) => { + throw new Error(e) + }) + if (history.result === "error") { + console.error(history.error) + return + } + // 入室 + ChatItemStore.setChatItems( + history.data.chatItems.map((chatItem) => ({ + ...chatItem, + status: "success", + })), + ) + history.data.pinnedChatItemIds.forEach((pinnedChatItem) => { + if (pinnedChatItem) { + PinnedChatItemsStore.add(pinnedChatItem) + } + }) + this.isRoomEnter = true } - if (this.room.state === "finished") { - // 本当はRESTでアーカイブデータを取ってきて表示する - } - // NOTE: もしかして:archivedも返ってくる? }, // socket.ioのセットアップ。配信を受け取る - socketSetUp() { + async socketSetUp() { // socket接続 this.$initSocket(UserItemStore.userItems.isAdmin) + const socket = await this.$socket() // SocketIOのコールバックの登録 - this.$socket().on("PUB_CHAT_ITEM", (chatItem) => { + socket.on("PUB_CHAT_ITEM", (chatItem) => { console.log(chatItem) // 自分が送信したChatItemであればupdate、他のユーザーが送信したchatItemであればaddを行う ChatItemStore.addOrUpdate({ ...chatItem, status: "success" }) }) - this.$socket().on("PUB_CHANGE_TOPIC_STATE", (res) => { + socket.on("PUB_CHANGE_TOPIC_STATE", (res) => { // クリックしたTopicのStateを変える TopicStateItemStore.change({ key: res.topicId, state: res.state }) }) // スタンプ通知時の、SocketIOのコールバックの登録 - this.$socket().on("PUB_STAMP", (stamps: StampModel[]) => { + socket.on("PUB_STAMP", (stamps: StampModel[]) => { stamps.forEach((stamp) => { StampStore.addOrUpdate(stamp) }) }) // アクティブユーザー数のSocketIOのコールバックの登録 - this.$socket().on("PUB_USER_COUNT", (res: PubUserCountParam) => { + socket.on("PUB_USER_COUNT", (res: PubUserCountParam) => { this.activeUserCount = res.activeUserCount }) // ピン留めアイテムのSocketIOのコールバックの登録 - this.$socket().on("PUB_PINNED_MESSAGE", (res: PubPinnedMessageParam) => { + socket.on("PUB_PINNED_MESSAGE", (res: PubPinnedMessageParam) => { if ( PinnedChatItemsStore.pinnedChatItems.find( (id) => id === res.chatItemId, @@ -224,9 +247,10 @@ export default Vue.extend({ this.hamburgerMenu = "menu" } }, - changeTopicState(topicId: number, state: TopicState) { + async changeTopicState(topicId: number, state: TopicState) { TopicStateItemStore.change({ key: topicId, state }) - this.$socket().emit( + const socket = await this.$socket() + socket.emit( "ADMIN_CHANGE_TOPIC_STATE", { state, @@ -243,9 +267,10 @@ export default Vue.extend({ this.enterRoom(UserItemStore.userItems.myIconId) }, // ルーム入室 - enterRoom(iconId: number) { - this.socketSetUp() - this.$socket().emit( + async enterRoom(iconId: number) { + await this.socketSetUp() + const socket = await this.$socket() + socket.emit( "ENTER_ROOM", { iconId, @@ -253,65 +278,55 @@ export default Vue.extend({ speakerTopicId: UserItemStore.userItems.speakerId, }, (res) => { - if (res.result === "error") { - console.error(res.error) - return - } - res.data.topicStates.forEach((topicState) => { - TopicStateItemStore.change({ - key: topicState.topicId, - state: topicState.state, - }) - }) - res.data.pinnedChatItemIds.forEach((pinnedChatItem) => { - if (pinnedChatItem) { - PinnedChatItemsStore.add(pinnedChatItem) - } - }) - ChatItemStore.setChatItems( - res.data.chatItems.map((chatItem) => ({ - ...chatItem, - status: "success", - })), - ) + this.getRoomInfo(res) }, ) - this.isRoomEnter = true }, // 管理者ルーム入室 - adminEnterRoom() { - this.socketSetUp() - this.$socket().emit( + async adminEnterRoom() { + await this.socketSetUp() + const socket = await this.$socket() + socket.emit( "ADMIN_ENTER_ROOM", { roomId: this.room.id, }, (res) => { - if (res.result === "error") { - console.error(res.error) - return - } - ChatItemStore.setChatItems( - res.data.chatItems.map((chatItem) => ({ - ...chatItem, - status: "success", - })), - ) - res.data.topicStates.forEach((topicState) => { - TopicStateItemStore.change({ - key: topicState.topicId, - state: topicState.state, - }) - }) - this.activeUserCount = res.data.activeUserCount + this.getRoomInfo(res) }, ) - this.isRoomEnter = true UserItemStore.changeMyIcon(0) }, + // 入室時のルームの情報を取得 + getRoomInfo(res: EnterRoomResponse) { + if (res.result === "error") { + console.error(res.error) + return + } + ChatItemStore.setChatItems( + res.data.chatItems.map((chatItem) => ({ + ...chatItem, + status: "success", + })), + ) + res.data.topicStates.forEach((topicState) => { + TopicStateItemStore.change({ + key: topicState.topicId, + state: topicState.state, + }) + }) + res.data.pinnedChatItemIds.forEach((pinnedChatItem) => { + if (pinnedChatItem) { + PinnedChatItemsStore.add(pinnedChatItem) + } + }) + this.activeUserCount = res.data.activeUserCount + this.isRoomEnter = true + }, // ルーム終了 - finishRoom() { - this.$socket().emit("ADMIN_FINISH_ROOM", {}, (res) => { + async finishRoom() { + const socket = await this.$socket() + socket.emit("ADMIN_FINISH_ROOM", {}, (res) => { console.log(res) }) this.roomState = "finished" @@ -324,13 +339,10 @@ export default Vue.extend({ startRoom() { // TODO: ルームの状態をindex、またはvuexでもつ this.$apiClient - .put( - { - pathname: "/room/:id/start", - params: { id: this.room.id }, - }, - {}, - ) + .put({ + pathname: "/room/:id/start", + params: { id: this.room.id }, + }) .then(() => { this.adminEnterRoom() this.roomState = "ongoing" diff --git a/app/front/pages/invited.vue b/app/front/pages/invited.vue index 7f9a1a34..45a2a013 100644 --- a/app/front/pages/invited.vue +++ b/app/front/pages/invited.vue @@ -39,13 +39,10 @@ export default Vue.extend({ InviteSuccess, }, async asyncData({ app, query }) { - const res = await app.$apiClient.get( - { - pathname: "/room/:id", - params: { id: query.roomId as string }, - }, - {}, - ) + const res = await app.$apiClient.get({ + pathname: "/room/:id", + params: { id: query.roomId as string }, + }) if (res.result === "error") { throw new Error("ルーム情報なし") } @@ -70,9 +67,14 @@ export default Vue.extend({ methods: { async regiaterAdmin() { const res = await this.$apiClient.post( - // @ts-ignore - `/room/${this.roomId}/invited?admin_invite_key=${this.adminInviteKey}`, + { + pathname: `/room/:id/invited`, + params: { id: this.roomId }, + }, {}, + { + admin_invite_key: this.adminInviteKey, + }, ) if (res.result === "error") { window.alert("処理に失敗しました") diff --git a/app/front/pages/room/create.vue b/app/front/pages/room/create.vue index ad7f279f..41901c89 100644 --- a/app/front/pages/room/create.vue +++ b/app/front/pages/room/create.vue @@ -62,10 +62,7 @@ ) " @keydown.delete=" - if ( - sessionList[idx].title.length === 0 && - composing == false - ) { + if (sessionList[idx].title.length === 0 && !composing) { removeSessionAndMoveFocus($event, idx, 'up') } " @@ -74,21 +71,23 @@ /> diff --git a/app/front/plugins/apiClient.ts b/app/front/plugins/apiClient.ts index 0be8facf..8508289a 100644 --- a/app/front/plugins/apiClient.ts +++ b/app/front/plugins/apiClient.ts @@ -5,18 +5,8 @@ const repositoryPlugin: Plugin = (context, inject) => { const axios = context.$axios.create({ timeout: 10 * 1000, }) - const apiClient = new ApiClient(axios) - // NOTE: idTokenの変更を監視して、変更があればAPIClientに反映する - context.store.watch( - (state) => state.auth._idToken, - (idToken: string | null) => { - apiClient.setToken(idToken) - }, - { - immediate: true, - }, - ) + const apiClient = new ApiClient(axios) inject("apiClient", apiClient) } diff --git a/app/front/plugins/socket.ts b/app/front/plugins/socket.ts index 2c178b39..43167aba 100644 --- a/app/front/plugins/socket.ts +++ b/app/front/plugins/socket.ts @@ -1,13 +1,16 @@ import { Plugin } from "@nuxt/types" import buildSocket, { SocketIOType } from "~/utils/socketIO" -const socketPlugin: Plugin = (context, inject) => { - let socket: SocketIOType | null = null + +const socketPlugin: Plugin = (_, inject) => { // socketを初期化する - inject("initSocket", (asAdmin: boolean) => { - socket = buildSocket(asAdmin ? context.store.state.auth._idToken : null) + const socket = new Promise((resolve) => { + inject("initSocket", async (asAdmin: boolean) => { + const socket: SocketIOType = await buildSocket(asAdmin) + resolve(socket) + }) }) // socketを取得する - inject("socket", () => socket) + inject("socket", async () => await socket) } export default socketPlugin @@ -15,18 +18,18 @@ export default socketPlugin declare module "vue/types/vue" { interface Vue { readonly $initSocket: (asAdmin?: boolean) => void - readonly $socket: () => SocketIOType + readonly $socket: () => Promise } } declare module "@nuxt/types" { interface NuxtAppOptions { readonly $initSocket: (idToken?: boolean) => void - readonly $socket: () => SocketIOType + readonly $socket: () => Promise } interface Context { readonly $initSocket: (asAdmin?: boolean) => void - readonly $socket: () => SocketIOType + readonly $socket: () => Promise } } diff --git a/app/front/store/auth.ts b/app/front/store/auth.ts index 6f332494..86593932 100644 --- a/app/front/store/auth.ts +++ b/app/front/store/auth.ts @@ -16,18 +16,13 @@ type AuthUser = { }) export default class Auth extends VuexModule { private _authUser: AuthUser | null = null - private _idToken: string | null = null public get authUser() { return this._authUser } - public get idToken() { - return this._idToken - } - public get isLoggedIn() { - return this._authUser != null && this._idToken != null + return this._authUser != null } @Mutation @@ -35,21 +30,13 @@ export default class Auth extends VuexModule { this._authUser = authUser } - @Mutation - private setIdToken(idToken: string | null) { - this._idToken = idToken - } - @Action({ rawError: true }) - public async onIdTokenChangedAction(res: { authUser?: firebase.User }) { - if (res.authUser == null) { + public onIdTokenChangedAction({ authUser }: { authUser?: firebase.User }) { + if (authUser == null) { this.setAuthUser(null) - this.setIdToken(null) return } - const { uid, email, displayName, photoURL, emailVerified } = res.authUser + const { uid, email, displayName, photoURL, emailVerified } = authUser this.setAuthUser({ uid, email, displayName, photoURL, emailVerified }) - const idToken = await res.authUser.getIdToken() - this.setIdToken(idToken) } } diff --git a/app/front/store/chatItems.ts b/app/front/store/chatItems.ts index facc29d2..cf269d18 100644 --- a/app/front/store/chatItems.ts +++ b/app/front/store/chatItems.ts @@ -1,7 +1,7 @@ import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators" import { ChatItemModel, ChatItemSenderType } from "sushi-chat-shared" import getUUID from "~/utils/getUUID" -import { AuthStore, UserItemStore } from "~/store" +import { UserItemStore } from "~/store" import buildSocket from "~/utils/socketIO" import emitAsync from "~/utils/emitAsync" @@ -86,8 +86,9 @@ export default class ChatItems extends VuexModule { target?: ChatItemModel }) { const id = getUUID() + const isAdmin = UserItemStore.userItems.isAdmin - const senderType: ChatItemSenderType = UserItemStore.userItems.isAdmin + const senderType: ChatItemSenderType = isAdmin ? "admin" : UserItemStore.userItems.speakerId === topicId ? "speaker" @@ -108,7 +109,7 @@ export default class ChatItems extends VuexModule { }) // サーバーに送信する - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(isAdmin) try { await emitAsync(socket, "POST_CHAT_ITEM", { id, @@ -126,6 +127,8 @@ export default class ChatItems extends VuexModule { @Action({ rawError: true }) public async postReaction({ message }: { message: ChatItemModel }) { const id = getUUID() + const isAdmin = UserItemStore.userItems.isAdmin + // ローカルに反映する this.add({ id, @@ -139,7 +142,7 @@ export default class ChatItems extends VuexModule { quote: message, }) // サーバーに反映する - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(isAdmin) try { await emitAsync(socket, "POST_CHAT_ITEM", { id, @@ -164,8 +167,9 @@ export default class ChatItems extends VuexModule { target?: ChatItemModel }) { const id = getUUID() + const isAdmin = UserItemStore.userItems.isAdmin - const senderType: ChatItemSenderType = UserItemStore.userItems.isAdmin + const senderType: ChatItemSenderType = isAdmin ? "admin" : UserItemStore.userItems.speakerId === topicId ? "speaker" @@ -185,7 +189,7 @@ export default class ChatItems extends VuexModule { quote: target, }) // サーバーに反映する - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(isAdmin) try { await emitAsync(socket, "POST_CHAT_ITEM", { id, @@ -211,6 +215,7 @@ export default class ChatItems extends VuexModule { target: ChatItemModel }) { const id = getUUID() + const isAdmin = UserItemStore.userItems.isAdmin const senderType: ChatItemSenderType = UserItemStore.userItems.isAdmin ? "admin" @@ -231,7 +236,7 @@ export default class ChatItems extends VuexModule { content: text, }) // サーバーに反映する - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(isAdmin) try { await emitAsync(socket, "POST_CHAT_ITEM", { id, @@ -248,7 +253,8 @@ export default class ChatItems extends VuexModule { @Action({ rawError: true }) public async retrySendChatItem({ chatItem }: { chatItem: ChatItemModel }) { - const socket = buildSocket(AuthStore.idToken) + const isAdmin = UserItemStore.userItems.isAdmin + const socket = await buildSocket(isAdmin) this.updateStatus({ id: chatItem.id, status: "loading" }) try { await emitAsync(socket, "POST_CHAT_ITEM", chatItem) diff --git a/app/front/store/pinnedChatItems.ts b/app/front/store/pinnedChatItems.ts index e6ae6519..ea97cc58 100644 --- a/app/front/store/pinnedChatItems.ts +++ b/app/front/store/pinnedChatItems.ts @@ -1,5 +1,5 @@ import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators" -import { AuthStore } from "~/store" +import { UserItemStore } from "~/store" import emitAsync from "~/utils/emitAsync" import buildSocket from "~/utils/socketIO" @@ -35,7 +35,7 @@ export default class PinnedChatItems extends VuexModule { topicId: number chatItemId: string }) { - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(UserItemStore.userItems.isAdmin) await emitAsync(socket, "POST_PINNED_MESSAGE", { topicId, chatItemId, diff --git a/app/front/store/stamps.ts b/app/front/store/stamps.ts index bd2604d9..9f66faac 100644 --- a/app/front/store/stamps.ts +++ b/app/front/store/stamps.ts @@ -2,7 +2,7 @@ import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators" import { StampModel } from "sushi-chat-shared" import getUUID from "~/utils/getUUID" import buildSocket from "~/utils/socketIO" -import { AuthStore } from "~/utils/store-accessor" +import { UserItemStore } from "~/utils/store-accessor" import emitAsync from "~/utils/emitAsync" @Module({ @@ -38,7 +38,7 @@ export default class Stamps extends VuexModule { @Action({ rawError: true }) public async sendFavorite(topicId: number) { - const socket = buildSocket(AuthStore.idToken) + const socket = await buildSocket(UserItemStore.userItems.isAdmin) const id = getUUID() // StampStoreは配信で追加する this.add({ diff --git a/app/front/utils/getIdToken.ts b/app/front/utils/getIdToken.ts new file mode 100644 index 00000000..daf517e9 --- /dev/null +++ b/app/front/utils/getIdToken.ts @@ -0,0 +1,8 @@ +import firebase from "firebase" + +const getIdToken = async () => { + const idToken = await firebase.auth().currentUser?.getIdToken() + return idToken ?? null +} + +export default getIdToken diff --git a/app/front/utils/socketIO.ts b/app/front/utils/socketIO.ts index a3c209b5..e10fd651 100644 --- a/app/front/utils/socketIO.ts +++ b/app/front/utils/socketIO.ts @@ -1,22 +1,31 @@ import { io, Socket } from "socket.io-client" import { ServerListenEventsMap, ServerPubEventsMap } from "sushi-chat-shared" +import getIdToken from "./getIdToken" -// 型自体もexportしておく export type SocketIOType = Socket let socket: SocketIOType | null = null -const buildSocket = ( - idToken?: string | null, -): Socket => { - socket = - socket ?? - io(process.env.apiBaseUrl as string, { - auth: { - token: idToken || null, - }, - withCredentials: true, - }) +const buildSocket = async ( + asAdmin: boolean, +): Promise> => { + const idToken = !asAdmin ? null : await getIdToken() + + // NOTE: キャッシュがあれば返す + if ( + socket != null && + "token" in socket.auth && + socket.auth.token === idToken + ) { + return socket + } + + socket = io(process.env.apiBaseUrl as string, { + auth: { + token: idToken || null, + }, + withCredentials: true, + }) return socket } export default buildSocket diff --git a/app/server/src/__test__/chat.ts b/app/server/src/__test__/chat.ts index d7499829..67e82f1e 100644 --- a/app/server/src/__test__/chat.ts +++ b/app/server/src/__test__/chat.ts @@ -19,14 +19,22 @@ import RoomFactory from "../infra/factory/RoomFactory" import AdminService from "../service/admin/AdminService" import { AdminEnterRoomResponse, + ChatItemModel, + EnterRoomResponse, ErrorResponse, PubChangeTopicStateParam, + PubChatItemParam, + PubPinnedMessageParam, + PubStampParam, RoomModel, + RoomState, ServerListenEventsMap, ServerPubEventsMap, + StampModel, SuccessResponse, } from "sushi-chat-shared" import delay from "../utils/delay" +import User from "../domain/user/User" describe("機能テスト", () => { const MATCHING = { @@ -36,6 +44,24 @@ describe("機能テスト", () => { TEXT: expect.stringMatching(/.+/), } + const SYSTEM_MESSAGE_CONTENT = { + start: + "【運営Bot】\n 発表が始まりました!\nコメントを投稿して盛り上げましょう 🎉🎉\n", + pause: "【運営Bot】\n 発表が中断されました", + restart: "【運営Bot】\n 発表が再開されました", + finish: + "【運営Bot】\n 発表が終了しました!\n(引き続きコメントを投稿いただけます)", + } + + const SYSTEM_MESSAGE_BASE: Omit = { + id: MATCHING.UUID, + createdAt: MATCHING.DATE, + type: "message", + senderType: "system", + iconId: User.SYSTEM_USER_ICON_ID.valueOf(), + timestamp: expect.any(Number), + } + // RESTクライアント let client: supertest.SuperTest // Socketサーバー @@ -47,6 +73,23 @@ describe("機能テスト", () => { let pgPool: PGPool let roomData: RoomModel + let messageId: string + let reactionId: string + let questionId: string + let answerId: string + let notOnGoingTopicMessageId: string + let message: ChatItemModel + let reaction: ChatItemModel + let question: ChatItemModel + let answer: ChatItemModel + let notOnGoingTopicMessage: ChatItemModel + let stampId: string + let stamps: StampModel[] + let history: { + chatItems: ChatItemModel[] + stamps: StampModel[] + pinnedChatItemIds: (string | null)[] + } // テストのセットアップ beforeAll(async (done) => { @@ -117,7 +160,7 @@ describe("機能テスト", () => { describe("room作成", () => { const title = "テストルーム" - const topics = [1, 2, 3].map((i) => ({ title: `テストトピック-${i}` })) + const topics = [1, 2, 3, 4].map((i) => ({ title: `テストトピック-${i}` })) const description = "これはテスト用のルームです。" test("正常系_管理者がroomを作成", async () => { @@ -207,6 +250,8 @@ describe("機能テスト", () => { describe("ユーザーがルームに入る", () => { afterAll(() => { + // このステップで定義したlistenerが残っていると以降のテストに支障が出るため解除する。 + // 以降のテストでも同様にlistenerの解除を行なっている。 clientSockets[0].off("PUB_USER_COUNT") }) @@ -233,7 +278,11 @@ describe("機能テスト", () => { test("正常系_一般ユーザーがルームに入る", async (resolve) => { clientSockets[0].emit( "ENTER_ROOM", - { roomId: roomData.id, iconId: 1, speakerTopicId: 1 }, + { + roomId: roomData.id, + iconId: 1, + speakerTopicId: roomData.topics[0].id, + }, (res) => { expect(res).toStrictEqual({ result: "success", @@ -277,7 +326,11 @@ describe("機能テスト", () => { }) clientSockets[1].emit( "ENTER_ROOM", - { roomId: roomData.id, iconId: 2, speakerTopicId: 1 }, + { + roomId: roomData.id, + iconId: 2, + speakerTopicId: roomData.topics[2].id, + }, // eslint-disable-next-line @typescript-eslint/no-empty-function () => {}, ) @@ -286,7 +339,9 @@ describe("機能テスト", () => { describe("トピックの状態遷移", () => { afterEach(async () => { + // listenerの解除 clientSockets[0].off("PUB_CHANGE_TOPIC_STATE") + // DBの処理に若干時間がかかるため、少し待つようにする await delay(100) }) @@ -417,352 +472,548 @@ describe("機能テスト", () => { ) }) }) - // - // describe("コメントを投稿する", () => { - // beforeAll(() => { - // clientSockets[2].emit( - // "ENTER_ROOM", - // { roomId, iconId: "3" }, - // (res: any) => {}, - // ) - // }) - // - // afterEach(() => { - // clientSockets[0].off("PUB_CHAT_ITEM") - // }) - // - // beforeEach(async () => await delay(100)) - // - // test("Messageの投稿", (resolve) => { - // clientSockets[0].on("PUB_CHAT_ITEM", (res) => { - // expect(res).toStrictEqual({ - // id: messageId, - // topicId: topics[0].id, - // type: "message", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "コメント", - // target: null, - // }) - // resolve() - // }) - // clientSockets[1].emit("POST_CHAT_ITEM", { - // id: messageId, - // topicId: topics[0].id, - // type: "message", - // content: "コメント", - // }) - // }) - // - // test("Reactionの投稿", (resolve) => { - // clientSockets[0].on("PUB_CHAT_ITEM", (res) => { - // expect(res).toStrictEqual({ - // id: reactionId, - // topicId: topics[0].id, - // type: "reaction", - // iconId: "3", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // target: { - // id: messageId, - // topicId: topics[0].id, - // type: "message", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "コメント", - // target: null, - // }, - // }) - // resolve() - // }) - // clientSockets[2].emit("POST_CHAT_ITEM", { - // id: reactionId, - // topicId: topics[0].id, - // type: "reaction", - // reactionToId: messageId, - // }) - // }) - // - // test("Questionの投稿", (resolve) => { - // clientSockets[0].on("PUB_CHAT_ITEM", (res) => { - // expect(res).toStrictEqual({ - // id: questionId, - // topicId: topics[0].id, - // type: "question", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "質問", - // }) - // resolve() - // }) - // clientSockets[1].emit("POST_CHAT_ITEM", { - // id: questionId, - // topicId: topics[0].id, - // type: "question", - // content: "質問", - // }) - // }) - // - // test("Answerの投稿", (resolve) => { - // clientSockets[0].on("PUB_CHAT_ITEM", (res) => { - // expect(res).toStrictEqual({ - // id: answerId, - // topicId: topics[0].id, - // type: "answer", - // iconId: "3", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "回答", - // target: { - // id: questionId, - // topicId: topics[0].id, - // type: "question", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "質問", - // }, - // }) - // resolve() - // }) - // clientSockets[2].emit("POST_CHAT_ITEM", { - // id: answerId, - // topicId: topics[0].id, - // type: "answer", - // content: "回答", - // target: questionId, - // }) - // }) - // }) - // - // describe("スタンプの投稿", () => { - // test("スタンプを投稿する", (resolve) => { - // clientSockets[0].on("PUB_STAMP", (res) => { - // expect(res).toStrictEqual([ - // { - // userId: clientSockets[2].id, - // timestamp: expect.any(Number), - // topicId: topics[0].id, - // }, - // ]) - // resolve() - // }) - // clientSockets[2].emit("POST_STAMP", { topicId: topics[0].id }) - // }) - // }) - // - // describe("途中から入室した場合", () => { - // beforeAll(async () => await delay(100)) - // - // test("途中から入室した場合に履歴が見れる", (resolve) => { - // clientSockets[3].emit( - // "ENTER_ROOM", - // { roomId, iconId: "4" }, - // (res: any) => { - // expect(res).toStrictEqual({ - // // NOTE: changeTopicStateで現在開いているトピックを閉じた際のbotメッセージと、次のトピックが開いた際の - // // botメッセージが同時に追加されるが、それらがDBに格納される順序が不安定だったため、順序を考慮しないように - // // している。アプリケーションの挙動としてはそれらは別トピックに投稿されるメッセージのため、問題はないはず。 - // chatItems: expect.arrayContaining([ - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "1", - // type: "message", - // content: - // "【運営Bot】\n 発表が始まりました!\nコメントを投稿して盛り上げましょう 🎉🎉\n", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "1", - // type: "message", - // content: - // "【運営Bot】\n 発表が終了しました!\n(引き続きコメントを投稿いただけます)", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "2", - // type: "message", - // content: - // "【運営Bot】\n 発表が始まりました!\nコメントを投稿して盛り上げましょう 🎉🎉\n", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "2", - // type: "message", - // content: - // "【運営Bot】\n 発表が終了しました!\n(引き続きコメントを投稿いただけます)", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "3", - // type: "message", - // content: - // "【運営Bot】\n 発表が始まりました!\nコメントを投稿して盛り上げましょう 🎉🎉\n", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "3", - // type: "message", - // content: "【運営Bot】\n 発表が中断されました", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "0", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: expect.any(String), - // topicId: "1", - // type: "message", - // content: "【運営Bot】\n 発表が再開されました", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "2", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: messageId, - // topicId: "1", - // type: "message", - // content: "コメント", - // target: null, - // }, - // { - // timestamp: expect.any(Number), - // iconId: "3", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // target: { - // id: messageId, - // topicId: topics[0].id, - // type: "message", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "コメント", - // target: null, - // }, - // id: reactionId, - // topicId: "1", - // type: "reaction", - // }, - // { - // timestamp: expect.any(Number), - // iconId: "2", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: questionId, - // topicId: "1", - // type: "question", - // content: "質問", - // }, - // { - // timestamp: expect.any(Number), - // iconId: "3", - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // id: answerId, - // topicId: "1", - // type: "answer", - // content: "回答", - // target: { - // id: questionId, - // topicId: topics[0].id, - // type: "question", - // iconId: "2", - // timestamp: expect.any(Number), - // createdAt: expect.stringMatching( - // /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/, - // ), - // content: "質問", - // }, - // }, - // ]), - // topics: [ - // { ...topics[0], state: "active" }, - // { ...topics[1], state: "finished" }, - // { ...topics[2], state: "paused" }, - // ...topics.slice(3), - // ], - // activeUserCount: 5, - // }) - // resolve() - // }, - // ) - // }) - // }) - // - // describe("ルームの終了・閉じる", () => { - // test("ルームを終了する", (resolve) => { - // clientSockets[0].on("PUB_FINISH_ROOM", () => { - // resolve() - // }) - // adminSocket.emit("ADMIN_FINISH_ROOM", {}) - // }) - // - // test("ルームを閉じる", (resolve) => { - // clientSockets[0].on("PUB_CLOSE_ROOM", () => { - // resolve() - // }) - // adminSocket.emit("ADMIN_CLOSE_ROOM", {}) - // }) - // }) + + // NOTE: roomData.topics[2]のトピックが進行中になっている前提 + describe("ChatItemの投稿", () => { + beforeAll(() => { + // await new Promise((resolve) => + // adminSocket.emit( + // "ADMIN_CHANGE_TOPIC_STATE", + // { + // topicId: roomData.topics[2].id, + // state: "ongoing", + // }, + // // eslint-disable-next-line @typescript-eslint/no-empty-function + // resolve, + // ), + // ) + + clientSockets[2].emit( + "ENTER_ROOM", + { + roomId: roomData.id, + iconId: 3, + speakerTopicId: roomData.topics[0].id, + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + afterEach(() => { + // listenerを解除 + clientSockets[0].off("PUB_CHAT_ITEM") + }) + + test("正常系_Messageの投稿", (resolve) => { + messageId = uuid() + + clientSockets[0].on("PUB_CHAT_ITEM", (res) => { + expect(res).toStrictEqual({ + id: messageId, + topicId: roomData.topics[2].id, + createdAt: MATCHING.DATE, + type: "message", + senderType: "speaker", + iconId: 2, + content: "コメント", + timestamp: expect.any(Number), + }) + message = res + resolve() + }) + clientSockets[1].emit( + "POST_CHAT_ITEM", + { + id: messageId, + topicId: roomData.topics[2].id, + type: "message", + content: "コメント", + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + test("正常系_Reactionの投稿", (resolve) => { + reactionId = uuid() + + clientSockets[0].on("PUB_CHAT_ITEM", (res) => { + expect(res).toStrictEqual({ + id: reactionId, + topicId: roomData.topics[2].id, + createdAt: MATCHING.DATE, + type: "reaction", + senderType: "general", + iconId: 3, + quote: message, + timestamp: expect.any(Number), + }) + reaction = res + resolve() + }) + clientSockets[2].emit( + "POST_CHAT_ITEM", + { + id: reactionId, + topicId: roomData.topics[2].id, + type: "reaction", + quoteId: messageId, + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + test("正常系_Questionの投稿", (resolve) => { + questionId = uuid() + + clientSockets[0].on("PUB_CHAT_ITEM", (res) => { + expect(res).toStrictEqual({ + id: questionId, + topicId: roomData.topics[2].id, + createdAt: MATCHING.DATE, + type: "question", + senderType: "speaker", + iconId: 2, + content: "質問", + timestamp: expect.any(Number), + }) + question = res + resolve() + }) + clientSockets[1].emit( + "POST_CHAT_ITEM", + { + id: questionId, + topicId: roomData.topics[2].id, + type: "question", + content: "質問", + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + test("正常系_Answerの投稿", (resolve) => { + answerId = uuid() + + clientSockets[0].on("PUB_CHAT_ITEM", (res) => { + expect(res).toStrictEqual({ + id: answerId, + topicId: roomData.topics[2].id, + createdAt: MATCHING.DATE, + type: "answer", + senderType: "general", + iconId: 3, + content: "回答", + quote: question, + timestamp: expect.any(Number), + }) + answer = res + resolve() + }) + clientSockets[2].emit( + "POST_CHAT_ITEM", + { + id: answerId, + topicId: roomData.topics[2].id, + type: "answer", + content: "回答", + quoteId: questionId, + }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + test("正常系_進行中でないtopicにも投稿できる", (resolve) => { + notOnGoingTopicMessageId = uuid() + + clientSockets[0].on("PUB_CHAT_ITEM", (res) => { + // NOTE: `timestamp: undefined` は存在しない + expect(res).toEqual({ + id: notOnGoingTopicMessageId, + topicId: roomData.topics[0].id, + createdAt: MATCHING.DATE, + type: "message", + senderType: "speaker", + iconId: 3, + content: "ongoingでないトピックへの投稿", + }) + notOnGoingTopicMessage = res + resolve() + }) + + clientSockets[2].emit( + "POST_CHAT_ITEM", + { + id: notOnGoingTopicMessageId, + topicId: roomData.topics[0].id, + type: "message", + content: "ongoingでないトピックへの投稿", + }, + (res) => { + expect(res).toStrictEqual({ + result: "success", + }) + }, + ) + }) + + test("異常系_存在しないtopicには投稿できない", (resolve) => { + const notExistTopicId = 10 + + clientSockets[1].emit( + "POST_CHAT_ITEM", + { + id: uuid(), + topicId: notExistTopicId, + type: "message", + content: "存在しないトピックへの投稿", + }, + (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { + code: MATCHING.CODE, + message: MATCHING.TEXT, + }, + }) + resolve() + }, + ) + }) + + test("異常系_未開始のtopicには投稿できない", (resolve) => { + clientSockets[1].emit( + "POST_CHAT_ITEM", + { + id: uuid(), + topicId: roomData.topics[3].id, + type: "message", + content: "未開始のtopicへの投稿", + }, + (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { + code: MATCHING.CODE, + message: MATCHING.TEXT, + }, + }) + resolve() + }, + ) + }) + }) + + describe("ChatItemをピン留め", () => { + afterEach(() => { + clientSockets[0].off("PUB_PINNED_MESSAGE") + }) + + test("正常系_speakerが進行中のTopicにChatItemをピン留めする", (resolve) => { + clientSockets[0].on("PUB_PINNED_MESSAGE", (res) => { + expect(res).toStrictEqual({ + topicId: roomData.topics[2].id, + chatItemId: messageId, + }) + resolve() + }) + clientSockets[1].emit( + "POST_PINNED_MESSAGE", + { topicId: roomData.topics[2].id, chatItemId: messageId }, + (res) => { + expect(res).toStrictEqual({ result: "success" }) + }, + ) + }) + + test("正常系_speakerが進行中でないTopicにChatItemをピン留めする", (resolve) => { + clientSockets[0].on("PUB_PINNED_MESSAGE", (res) => { + expect(res).toStrictEqual({ + topicId: roomData.topics[0].id, + chatItemId: notOnGoingTopicMessageId, + }) + resolve() + }) + clientSockets[2].emit( + "POST_PINNED_MESSAGE", + { + topicId: roomData.topics[0].id, + chatItemId: notOnGoingTopicMessageId, + }, + (res) => { + expect(res).toStrictEqual({ result: "success" }) + }, + ) + }) + + // TODO: アプリケーション側が未対応 + test.skip("異常系_speaker以外はピン留めできない", (resolve) => { + clientSockets[2].emit( + "POST_PINNED_MESSAGE", + { topicId: roomData.topics[2].id, chatItemId: messageId }, + (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { code: MATCHING.CODE, message: MATCHING.TEXT }, + }) + resolve() + }, + ) + }) + }) + + describe("Stampの投稿", () => { + afterAll(() => { + // listenerを解除 + clientSockets[0].off("PUB_STAMP") + }) + + test("正常系_スタンプを投稿する", (resolve) => { + stampId = uuid() + + clientSockets[0].on("PUB_STAMP", (res) => { + expect(res).toStrictEqual([ + { + id: MATCHING.UUID, + topicId: roomData.topics[2].id, + timestamp: expect.any(Number), + createdAt: MATCHING.DATE, + }, + ]) + stamps = res + resolve() + }) + clientSockets[1].emit( + "POST_STAMP", + { id: stampId, topicId: roomData.topics[2].id }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + () => {}, + ) + }) + + // FIXME: アプリケーションの方が未対応 + test.skip("異常系_進行中でないトピックにはスタンプを投稿できない", (resolve) => { + const dummyStampId = uuid() + + clientSockets[1].emit( + "POST_STAMP", + { id: dummyStampId, topicId: roomData.topics[0].id }, + (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { code: MATCHING.CODE, message: MATCHING.TEXT }, + }) + resolve() + }, + ) + }) + + test.skip("異常系_存在しないトピックにはスタンプを投稿できない", (resolve) => { + const dummyStampId = uuid() + const notExistTopicId = 10 + + clientSockets[1].emit( + "POST_STAMP", + { id: dummyStampId, topicId: notExistTopicId }, + (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { code: MATCHING.CODE, message: MATCHING.TEXT }, + }) + resolve() + }, + ) + }) + }) + + describe("途中からルームに入る", () => { + beforeAll(() => { + history = { + // トピックの終了と開始が同時に発生する時、system messageの順番を仕様上規定していないので、 + // 順番を考慮しないようにarrayContainingを使っている + // chatItems: expect.arrayContaining([ + chatItems: [ + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[0].id, + content: SYSTEM_MESSAGE_CONTENT.start, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[0].id, + content: SYSTEM_MESSAGE_CONTENT.finish, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[1].id, + content: SYSTEM_MESSAGE_CONTENT.start, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[1].id, + content: SYSTEM_MESSAGE_CONTENT.finish, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[2].id, + content: SYSTEM_MESSAGE_CONTENT.start, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[2].id, + content: SYSTEM_MESSAGE_CONTENT.pause, + }, + { + ...SYSTEM_MESSAGE_BASE, + topicId: roomData.topics[2].id, + content: SYSTEM_MESSAGE_CONTENT.restart, + }, + message, + reaction, + question, + answer, + notOnGoingTopicMessage, + ], + stamps, + pinnedChatItemIds: [notOnGoingTopicMessageId, null, messageId, null], + } + }) + + test("正常系_チャットやスタンプの履歴が見れる", (resolve) => { + clientSockets[3].emit( + "ENTER_ROOM", + { + roomId: roomData.id, + iconId: 4, + speakerTopicId: roomData.topics[0].id, + }, + (res) => { + expect(res).toEqual({ + result: "success", + data: { + chatItems: expect.arrayContaining(history.chatItems), + stamps: history.stamps, + pinnedChatItemIds: history.pinnedChatItemIds, + // ルームに参加しているユーザー(管理者ユーザー + 一般ユーザー) + システムユーザー + activeUserCount: 1 + clientSockets.length + 1, + topicStates: [ + { + topicId: roomData.topics[0].id, + state: "finished", + }, + { topicId: roomData.topics[1].id, state: "finished" }, + + { topicId: roomData.topics[2].id, state: "ongoing" }, + { topicId: roomData.topics[3].id, state: "not-started" }, + ], + }, + }) + resolve() + }, + ) + }) + }) + + describe("roomの終了", () => { + test("正常系_roomを終了する", (done) => { + adminSocket.emit("ADMIN_FINISH_ROOM", {}, async (res) => { + expect(res).toStrictEqual({ + result: "success", + }) + + const roomRes = await client.get(`/room/${roomData.id}`) + expect(roomRes.body.data.state).toBe("finished") + + done() + }) + }) + + // TODO: アプリケーション側で500エラーを返すようにしてしまっていたので、そちらを修正したらskipを外す + test.skip("異常系_終了しているroomを終了しようとするとエラーが返る", (done) => { + adminSocket.emit("ADMIN_FINISH_ROOM", {}, async (res) => { + expect(res).toStrictEqual({ + result: "error", + error: { + code: "400", + message: expect.any(String), + }, + }) + + done() + }) + }) + }) + + describe("roomの履歴確認", () => { + // TODO: 結果が不安定でたまに失敗する(DBへのインサートの順序が実行時依存なのがおそらくの原因)のでskipにしている。 + // このエンドポイントのレスポンスの方を仕様通りに直せばchatItemsのパラメータにexpect.arrayContaining()が使えるので、 + // おそらくはそれで解決できそう。 + test.skip("正常系_終了したルームのチャットアイテムの履歴を見れる", async () => { + const res = await client.get(`/room/${roomData.id}/history`) + + expect(res.statusCode).toBe(200) + expect(res.body).toMatchObject({ + result: "success", + data: { + chatItems: history.chatItems.map((c) => { + if (!c.quote) return c + + const chatItem: Record = { + createdAt: c.createdAt, + id: c.id, + senderType: c.senderType, + timestamp: c.timestamp, + topicId: c.topicId, + } + if (c.type === "message") { + chatItem.content = c.content + } + return chatItem + }), + pinnedChatItemIds: history.pinnedChatItemIds.filter( + (p): p is string => p !== null, + ), + stamps: history.stamps, + }, + }) + }) + + // TODO: アプリケーション側で適切なステータスコードを返すようになっていないので修正したらskipを外す + test.skip("異常系_存在しないルームの履歴は見れない", async () => { + const notExistRoomId = uuid() + const res = await client.get(`/room/${notExistRoomId}/history`) + + expect(res.statusCode).toBe(404) + }) + }) + + describe("roomの公開停止", () => { + test("正常系_ルームを公開停止できる", async () => { + const res = await client + .put(`/room/${roomData.id}/archive`) + .set("Authorization", "Bearer token") + expect(res.statusCode).toBe(200) + + const room = await client.get(`/room/${roomData.id}`) + expect(room.body.data.state).toBe("archived") + }) + + // TODO: アプリケーション側で適切なステータスコードを返すようになっていないので修正したらskipを外す + test.skip("異常系_存在しないルームは公開停止できない", async () => { + const notExistRoomId = uuid() + const res = await client + .put(`/room/${notExistRoomId}/archive`) + .set("Authorization", "Bearer token") + expect(res.statusCode).toBe(404) + }) + + test("異常系_管理者ユーザーのトークンを渡さないとルームを公開停止できない", async () => { + const res = await client.put(`/room/${roomData.id}/archive`) + expect(res.statusCode).toBe(401) + }) + }) }) diff --git a/app/server/src/__test__/testAdminService.ts b/app/server/src/__test__/testAdminService.ts index 37141eaf..3e2a77eb 100644 --- a/app/server/src/__test__/testAdminService.ts +++ b/app/server/src/__test__/testAdminService.ts @@ -59,7 +59,7 @@ describe("AdminServiceのテスト", () => { }) }) - describe("getManagedRoomsのテスト", () => { + describe("fetchManagedRoomsのテスト", () => { test("正常系_管理しているroom一覧を取得する", async () => { // adminが既に登録されている状態にしておく adminRepository.createIfNotExist(admin) @@ -117,5 +117,12 @@ describe("AdminServiceのテスト", () => { expect(r.startAt).toBeNull() }) }) + + test("異常系_存在しないadminIdならエラーを投げる", async () => { + const notExistAdminId = uuid() + await expect(() => + adminService.getManagedRooms({ adminId: notExistAdminId }), + ).rejects.toThrow() + }) }) }) diff --git a/app/server/src/__test__/testChatItemService.ts b/app/server/src/__test__/testChatItemService.ts index a433e68f..3f8b982a 100644 --- a/app/server/src/__test__/testChatItemService.ts +++ b/app/server/src/__test__/testChatItemService.ts @@ -65,7 +65,7 @@ describe("ChatItemServiceのテスト", () => { "test room", uuid(), "This is test room.", - [{ title: "test topic" }], + [{ title: "test topic", state: "ongoing" }], new Set([admin.id]), "ongoing", new Date(), @@ -163,6 +163,19 @@ describe("ChatItemServiceのテスト", () => { expect(deliveredMessage.content).toBe("テストメッセージ") expect(deliveredMessage.quote?.id).toBe(target.id) }) + + test("異常系_存在しないuserIdならエラーを投げる", async () => { + const notExistUserId = uuid() + await expect( + chatItemService.postMessage({ + chatItemId: uuid(), + userId: notExistUserId, + topicId: 1, + content: "テストメッセージ", + quoteId: null, + }), + ).rejects.toThrow() + }) }) describe("postReactionのテスト", () => { @@ -199,6 +212,19 @@ describe("ChatItemServiceのテスト", () => { expect(deliveredReaction.isPinned).toBeFalsy() expect(deliveredReaction.quote?.id).toBe(target.id) }) + + test("異常系_存在しないuserIdならエラーを投げる", async () => { + const notExistUserId = uuid() + await expect( + chatItemService.postMessage({ + chatItemId: uuid(), + userId: notExistUserId, + topicId: 1, + content: "テストメッセージ", + quoteId: null, + }), + ).rejects.toThrow() + }) }) describe("postQuestionのテスト", () => { @@ -236,6 +262,19 @@ describe("ChatItemServiceのテスト", () => { expect(deliveredQuestion.isPinned).toBeFalsy() expect(deliveredQuestion.content).toBe("テストクエスチョン") }) + + test("異常系_存在しないuserIdならエラーを投げる", async () => { + const notExistUserId = uuid() + await expect( + chatItemService.postMessage({ + chatItemId: uuid(), + userId: notExistUserId, + topicId: 1, + content: "テストメッセージ", + quoteId: null, + }), + ).rejects.toThrow() + }) }) describe("postAnswerのテスト", () => { @@ -275,6 +314,19 @@ describe("ChatItemServiceのテスト", () => { expect(deliveredAnswer.content).toBe("テストアンサー") expect((deliveredAnswer.quote as Question).id).toBe(target.id) }) + + test("異常系_存在しないuserIdならエラーを投げる", async () => { + const notExistUserId = uuid() + await expect( + chatItemService.postMessage({ + chatItemId: uuid(), + userId: notExistUserId, + topicId: 1, + content: "テストメッセージ", + quoteId: null, + }), + ).rejects.toThrow() + }) }) describe("pinChatItemのテスト", () => { @@ -284,5 +336,12 @@ describe("ChatItemServiceのテスト", () => { const pinned = await chatItemRepository.find(target.id) expect(pinned?.isPinned).toBeTruthy() }) + + test("異常系_存在しないchatItemIdならエラーを投げる", async () => { + const notExistChatItemId = uuid() + await expect( + chatItemService.pinChatItem({ chatItemId: notExistChatItemId }), + ).rejects.toThrow() + }) }) }) diff --git a/app/server/src/__test__/testRealTimeRoomService.ts b/app/server/src/__test__/testRealTimeRoomService.ts index 0b26039b..6b2c6e07 100644 --- a/app/server/src/__test__/testRealTimeRoomService.ts +++ b/app/server/src/__test__/testRealTimeRoomService.ts @@ -20,7 +20,9 @@ import EphemeralAdminRepository from "../infra/repository/admin/EphemeralAdminRe import Admin from "../domain/admin/admin" describe("RealtimeRoomServiceのテスト", () => { - let adminUser: User + let roomId: string + let realtimeAdmin: User + let normalUser: User let topics: PartiallyPartial[] let roomRepository: IRoomRepository @@ -52,19 +54,16 @@ describe("RealtimeRoomServiceのテスト", () => { new EphemeralChatItemDelivery(chatItemDeliverySubscribers), ) - const admin = new Admin(uuid(), "Admin", []) + const adminId = uuid() + const admin = new Admin(adminId, "Admin", []) adminRepository.createIfNotExist(admin) - const adminUserId = uuid() - const roomId = uuid() - // WebSocket接続するadmin user - adminUser = new User(adminUserId, true, false, roomId, User.ADMIN_ICON_ID) - userRepository.create(adminUser) - + roomId = uuid() const title = "テストルーム" const inviteKey = uuid() const description = "テスト用のルームです" topics = [1, 2].map((i) => ({ title: `テストトピック${i}` })) + const adminIds = new Set([admin.id]) const state: RoomState = "ongoing" const startAt = new Date() const room = new RoomClass( @@ -73,11 +72,29 @@ describe("RealtimeRoomServiceのテスト", () => { inviteKey, description, topics, - new Set([admin.id]), + adminIds, state, startAt, ) roomRepository.build(room) + + // WebSocket接続するadmin user + const realtimeAdminId = uuid() + realtimeAdmin = new User( + realtimeAdminId, + true, + false, + roomId, + User.ADMIN_ICON_ID, + ) + userRepository.create(realtimeAdmin) + room.joinAdminUser(realtimeAdminId, adminId) + + // WebSocket接続する一般user + const normalUserId = uuid() + normalUser = new User(normalUserId, false, false, roomId, NewIconId(1)) + userRepository.create(normalUser) + room.joinUser(normalUserId) }) describe("changeTopicStateのテスト", () => { @@ -86,7 +103,7 @@ describe("RealtimeRoomServiceのテスト", () => { beforeEach(async () => { await roomService.changeTopicState({ - userId: adminUser.id, + userId: realtimeAdmin.id, topicId: 1, state: "ongoing", }) @@ -98,14 +115,14 @@ describe("RealtimeRoomServiceのテスト", () => { test("正常系_進行中のtopicが終了して次のtopicが開始する", async () => { const nextTopicId = 2 await roomService.changeTopicState({ - userId: adminUser.id, + userId: realtimeAdmin.id, topicId: nextTopicId, state: "ongoing", }) - const room = await roomRepository.find(adminUser.roomId) + const room = await roomRepository.find(roomId) if (!room) { - throw new Error(`Room(${adminUser.roomId}) was not found.`) + throw new Error(`Room(${roomId}) was not found.`) } expect(room.topics[0].state).toBe("finished") @@ -122,7 +139,7 @@ describe("RealtimeRoomServiceのテスト", () => { expect( deliveredFinishContent.content, ).toStrictEqual({ - roomId: adminUser.roomId, + roomId, topic: { ...topics[0], id: 1, @@ -138,7 +155,7 @@ describe("RealtimeRoomServiceのテスト", () => { expect( deliveredOngoingContent.content, ).toStrictEqual({ - roomId: adminUser.roomId, + roomId, topic: { ...topics[1], id: 2, @@ -156,14 +173,14 @@ describe("RealtimeRoomServiceのテスト", () => { test("正常系_進行中のtopicが終了する", async () => { const currentTopicId = 1 await roomService.changeTopicState({ - userId: adminUser.id, + userId: realtimeAdmin.id, topicId: currentTopicId, state: "finished", }) - const room = await roomRepository.find(adminUser.roomId) + const room = await roomRepository.find(roomId) if (!room) { - throw new Error(`Room(${adminUser.roomId}) was not found.`) + throw new Error(`Room(${roomId}) was not found.`) } expect(room.topics[0].state).toBe("finished") @@ -175,7 +192,7 @@ describe("RealtimeRoomServiceのテスト", () => { roomDeliverySubscribers[0][deliveredRoomContentsCount] expect(deliveredContent.type).toBe("CHANGE_TOPIC_STATE") expect(deliveredContent.content).toStrictEqual({ - roomId: adminUser.roomId, + roomId, topic: { ...topics[0], id: 1, @@ -193,14 +210,14 @@ describe("RealtimeRoomServiceのテスト", () => { test("正常系_進行中のtopicが一時停止する", async () => { const currentTopicId = 1 await roomService.changeTopicState({ - userId: adminUser.id, + userId: realtimeAdmin.id, topicId: currentTopicId, state: "paused", }) - const room = await roomRepository.find(adminUser.roomId) + const room = await roomRepository.find(roomId) if (!room) { - throw new Error(`Room(${adminUser.roomId}) was not found.`) + throw new Error(`Room(${roomId}) was not found.`) } expect(room.topics[0].state).toBe("paused") @@ -212,7 +229,7 @@ describe("RealtimeRoomServiceのテスト", () => { roomDeliverySubscribers[0][deliveredRoomContentsCount] expect(deliveredContent.type).toBe("CHANGE_TOPIC_STATE") expect(deliveredContent.content).toStrictEqual({ - roomId: adminUser.roomId, + roomId: roomId, topic: { ...topics[0], id: 1, @@ -226,15 +243,29 @@ describe("RealtimeRoomServiceのテスト", () => { deliveredChatItemContentsCount + 1, ) }) + + test("異常系_存在しないuserはトピックの状態を変更できない", async () => { + const notExistUserId = uuid() + + await expect(() => + roomService.finish({ userId: notExistUserId }), + ).rejects.toThrow() + }) + + test("異常系_adminでないuserはトピックの状態を変更できない", async () => { + await expect(() => + roomService.finish({ userId: normalUser.id }), + ).rejects.toThrow() + }) }) describe("finishのテスト", () => { test("正常系_roomが終了する", async () => { - await roomService.finish({ userId: adminUser.id }) + await roomService.finish({ userId: realtimeAdmin.id }) - const room = await roomRepository.find(adminUser.roomId) + const room = await roomRepository.find(roomId) if (!room) { - throw new Error(`Room(${adminUser.roomId}) was not found.`) + throw new Error(`Room(${roomId}) was not found.`) } expect(room.state).toBe("finished") @@ -246,21 +277,13 @@ describe("RealtimeRoomServiceのテスト", () => { await expect(() => roomService.finish({ userId: notExistUserId }), - ).rejects.toThrowError() + ).rejects.toThrow() }) test("異常系_adminでないuserはroomをfinishできない", async () => { - const notAdminUser = new User( - uuid(), - false, - false, - adminUser.roomId, - NewIconId(1), - ) - await expect(() => - roomService.finish({ userId: notAdminUser.id }), - ).rejects.toThrowError() + roomService.finish({ userId: normalUser.id }), + ).rejects.toThrow() }) }) }) diff --git a/app/server/src/__test__/testRestRoomService.ts b/app/server/src/__test__/testRestRoomService.ts index fd95dbea..d642bdb3 100644 --- a/app/server/src/__test__/testRestRoomService.ts +++ b/app/server/src/__test__/testRestRoomService.ts @@ -79,7 +79,6 @@ describe("RestRoomServiceのテスト", () => { describe("startのテスト", () => { test("正常系_roomがstartする", async () => { - // startされるroomを作成しておく const room = new RoomClass( roomId, title, @@ -88,6 +87,8 @@ describe("RestRoomServiceのテスト", () => { topics, adminIds, ) + + // startされるroomを作成しておく roomRepository.build(room) expect(room.state).toBe("not-started") @@ -103,6 +104,89 @@ describe("RestRoomServiceのテスト", () => { expect(startedRoom.state).toBe("ongoing") expect(startedRoom.startAt).not.toBeNull() }) + + test("異常系_存在しないroomはstartできない", async () => { + const notExistRoomId = uuid() + await expect(() => + roomService.start({ id: notExistRoomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_admin以外はstartできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + ) + + // startされるroomを作成しておく + roomRepository.build(room) + + const notExistAdminId = uuid() + await expect(() => + roomService.start({ id: roomId, adminId: notExistAdminId }), + ).rejects.toThrow() + }) + + test("異常系_すでにstartしたroomはstartできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "ongoing", + ) + + // startされるroomを作成しておく + roomRepository.build(room) + + await expect( + roomService.start({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_終了したroomはstartできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "finished", + ) + + // startされるroomを作成しておく + roomRepository.build(room) + + await expect( + roomService.start({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_archiveしたroomはstartできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "archived", + ) + + // startされるroomを作成しておく + roomRepository.build(room) + + await expect( + roomService.start({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) }) describe("inviteAdminのテスト", () => { @@ -135,6 +219,32 @@ describe("RestRoomServiceのテスト", () => { expect(invitedRoom.adminIds.size).toBe(2) expect(invitedRoom.adminIds.has(anotherAdminId)).toBeTruthy() }) + + test("異常系_存在しないroomにはinviteできない", async () => { + const notExistRoomId = uuid() + const anotherAdminId = uuid() + + await expect(() => + roomService.inviteAdmin({ + id: notExistRoomId, + adminId: anotherAdminId, + adminInviteKey: inviteKey, + }), + ).rejects.toThrow() + }) + + test("異常系_不正なinviteKeyではinviteできない", async () => { + const anotherAdminId = uuid() + const invalidInviteKey = uuid() + + await expect(() => + roomService.inviteAdmin({ + id: roomId, + adminId: anotherAdminId, + adminInviteKey: invalidInviteKey, + }), + ).rejects.toThrow() + }) }) describe("archiveのテスト", () => { @@ -161,6 +271,94 @@ describe("RestRoomServiceのテスト", () => { expect(room.state).toBe("archived") expect(room.archivedAt).not.toBeNull() }) + + test("異常系_存在しないRoomはarchiveできない", async () => { + const notExistRoomId = uuid() + await expect(() => + roomService.archive({ id: notExistRoomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_admin以外はarchiveできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "finished", + startAt, + [], + finishAt, + ) + roomRepository.build(room) + + const notExistAdminId = uuid() + await expect(() => + roomService.archive({ id: roomId, adminId: notExistAdminId }), + ).rejects.toThrow() + }) + + test("異常系_startしていないroomはarchiveできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "not-started", + startAt, + [], + finishAt, + ) + roomRepository.build(room) + + await expect(() => + roomService.archive({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_進行中のroomはarchiveできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "ongoing", + startAt, + [], + finishAt, + ) + roomRepository.build(room) + + await expect(() => + roomService.archive({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) + + test("異常系_すでにarchiveされたroomはarchiveできない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + "archived", + startAt, + [], + finishAt, + ) + roomRepository.build(room) + + await expect(() => + roomService.archive({ id: roomId, adminId: admin.id }), + ).rejects.toThrow() + }) }) describe("checkAdminAndFindのテスト", () => { @@ -224,6 +422,45 @@ describe("RestRoomServiceのテスト", () => { startDate: undefined, }) }) + + test("異常系_不正なuserIdが渡されても秘匿情報を返さない", async () => { + const room = new RoomClass( + roomId, + title, + inviteKey, + description, + topics, + adminIds, + ) + roomRepository.build(room) + + const notExistAdminId = uuid() + const res = await roomService.checkAdminAndfind({ + id: roomId, + adminId: notExistAdminId, + }) + + expect(res).toStrictEqual({ + id: roomId, + title, + description, + topics: topics.map((topic, i) => ({ + id: i + 1, + order: i + 1, + title: topic.title, + })), + state: "not-started", + adminInviteKey: undefined, + startDate: undefined, + }) + }) + + test("異常系_存在しないroomの情報は取得できない", async () => { + const notExistRoomId = uuid() + await expect(() => + roomService.checkAdminAndfind({ id: notExistRoomId }), + ).rejects.toThrow() + }) }) describe("findのテスト", () => { @@ -244,5 +481,10 @@ describe("RestRoomServiceのテスト", () => { expect(res.stamps).toStrictEqual([]) expect(res.pinnedChatItemIds).toStrictEqual([1, 2].map(() => null)) }) + + test("異常系_存在しないroomの履歴は取得できない", async () => { + const notExistRoomId = uuid() + await expect(() => roomService.find(notExistRoomId)).rejects.toThrow() + }) }) }) diff --git a/app/server/src/__test__/testStampService.ts b/app/server/src/__test__/testStampService.ts index 327f3778..4d642136 100644 --- a/app/server/src/__test__/testStampService.ts +++ b/app/server/src/__test__/testStampService.ts @@ -13,12 +13,14 @@ import { v4 as uuid } from "uuid" import User from "../domain/user/User" import { NewIconId } from "../domain/user/IconId" import RoomClass from "../domain/room/Room" +import IUserRepository from "../domain/user/IUserRepository" describe("StampServiceのテスト", () => { let userId: string let roomId: string let stampRepository: IStampRepository + let userRepository: IUserRepository let stampDeliverySubscriber: Stamp[] let stampService: StampService @@ -28,7 +30,7 @@ describe("StampServiceのテスト", () => { const adminRepository = new EphemeralAdminRepository() const roomRepository = new EphemeralRoomRepository(adminRepository) - const userRepository = new EphemeralUserRepository() + userRepository = new EphemeralUserRepository() stampRepository = new EphemeralStampRepository() stampDeliverySubscriber = [] @@ -58,7 +60,10 @@ describe("StampServiceのテスト", () => { "test room", uuid(), "This is test room.", - [{ title: "test topic" }], + [ + { id: 1, title: "test topic", state: "ongoing" }, + { id: 2, title: "test topic 2", state: "not-started" }, + ], new Set([admin.id]), "ongoing", new Date(), @@ -75,8 +80,7 @@ describe("StampServiceのテスト", () => { describe("postのテスト", () => { test("正常系_stampを投稿する", async () => { - const stampId = uuid() - await stampService.post({ id: stampId, userId, topicId: 1 }) + await stampService.post({ id: uuid(), userId, topicId: 1 }) // stampIntervalDeliveryのインターバル処理でスタンプで配信されるのを待つ await delay(200) @@ -88,11 +92,17 @@ describe("StampServiceのテスト", () => { expect(deliveredStamp.userId).toBe(userId) }) - test("異常系_OPEN状態でないTopicへはスタンプを投稿できない", async () => { - const stampId = uuid() + test("異常系_存在しないuserはstampを投稿できない", async () => { + const notExistUserId = uuid() await expect(() => - stampService.post({ id: stampId, userId, topicId: 2 }), - ).rejects.toThrowError() + stampService.post({ id: uuid(), userId: notExistUserId, topicId: 1 }), + ).rejects.toThrow() + }) + + test("異常系_ONGOING状態でないTopicへはスタンプを投稿できない", async () => { + await expect(() => + stampService.post({ id: uuid(), userId, topicId: 2 }), + ).rejects.toThrow() }) }) }) diff --git a/app/server/src/__test__/testUserService.ts b/app/server/src/__test__/testUserService.ts index aeac742c..2083e907 100644 --- a/app/server/src/__test__/testUserService.ts +++ b/app/server/src/__test__/testUserService.ts @@ -17,8 +17,10 @@ import Admin from "../domain/admin/admin" describe("UserServiceのテスト", () => { let adminId: string let roomId: string - let userId: string - let iconId: IconId + let adminUserId: string + let adminUserIconId: IconId + let normalUserId: string + let normalUserIconId: IconId let userRepository: IUserRepository let roomRepository: IRoomRepository @@ -46,8 +48,12 @@ describe("UserServiceのテスト", () => { adminRepository.createIfNotExist(new Admin(adminId, adminName, [])) roomId = uuid() - userId = uuid() - iconId = NewIconId(Math.floor(Math.random() * 9) + 1) + + adminUserId = uuid() + adminUserIconId = NewIconId(Math.floor(Math.random() * 10) + 1) + + normalUserId = uuid() + normalUserIconId = NewIconId(Math.floor(Math.random() * 10) + 1) }) describe("adminEnterRoomのテスト", () => { @@ -63,18 +69,96 @@ describe("UserServiceのテスト", () => { ) roomRepository.build(room) - await userService.adminEnterRoom({ roomId, userId, idToken: "" }) + await userService.adminEnterRoom({ + roomId, + userId: adminUserId, + idToken: "", + }) - const user = await userRepository.find(userId) - if (!user) { - throw new Error(`User(${userId}) was not found.`) + const adminUser = await userRepository.find(adminUserId) + if (!adminUser) { + throw new Error(`User(${adminUserId}) was not found.`) } - expect(user.id).toBe(userId) - expect(user.roomId).toBe(roomId) - expect(user.speakAt).toBeUndefined() - expect(user.iconId).toBe(User.ADMIN_ICON_ID) - expect(user.isAdmin).toBeTruthy() + expect(adminUser.id).toBe(adminUserId) + expect(adminUser.roomId).toBe(roomId) + expect(adminUser.speakAt).toBeUndefined() + expect(adminUser.iconId).toBe(User.ADMIN_ICON_ID) + expect(adminUser.isAdmin).toBeTruthy() + }) + + test("異常系_存在しないroomに参加しようとするとエラーになる", async () => { + const notExistRoomId = uuid() + await expect(() => + userService.adminEnterRoom({ + roomId: notExistRoomId, + userId: adminUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_開始していないroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "not-started", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: adminUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_終了したroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "finished", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: adminUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_アーカイブされたroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "archived", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: adminUserId, + idToken: "", + }), + ).rejects.toThrow() }) }) @@ -91,19 +175,96 @@ describe("UserServiceのテスト", () => { ) roomRepository.build(room) - const iconId = 1 - await userService.enterRoom({ userId, roomId, iconId }) + await userService.enterRoom({ + userId: normalUserId, + roomId, + iconId: normalUserIconId.valueOf(), + }) - const user = await userRepository.find(userId) - if (!user) { - throw new Error(`User(${userId}) was not found.`) + const normalUser = await userRepository.find(normalUserId) + if (!normalUser) { + throw new Error(`User(${normalUserId}) was not found.`) } - expect(user.id).toBe(userId) - expect(user.roomId).toBe(roomId) - expect(user.speakAt).toBeUndefined() - expect(user.iconId).toBe(iconId) - expect(user.isAdmin).toBeFalsy() + expect(normalUser.id).toBe(normalUserId) + expect(normalUser.roomId).toBe(roomId) + expect(normalUser.speakAt).toBeUndefined() + expect(normalUser.iconId).toBe(normalUserIconId) + expect(normalUser.isAdmin).toBeFalsy() + }) + + test("異常系_存在しないroomに参加しようとするとエラーになる", async () => { + const notExistRoomId = uuid() + await expect(() => + userService.adminEnterRoom({ + roomId: notExistRoomId, + userId: normalUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_開始していないroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "not-started", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: normalUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_終了したroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "finished", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: normalUserId, + idToken: "", + }), + ).rejects.toThrow() + }) + + test("異常系_アーカイブされたroomに参加しようとするとエラーになる", async () => { + const room = new RoomClass( + roomId, + "テストルーム", + uuid(), + "テスト用のルームです。", + [1, 2].map((i) => ({ title: `テストトピック${i}` })), + new Set([adminId]), + "archived", + ) + roomRepository.build(room) + + await expect(() => + userService.adminEnterRoom({ + roomId, + userId: normalUserId, + idToken: "", + }), + ).rejects.toThrow() }) }) @@ -121,31 +282,38 @@ describe("UserServiceのテスト", () => { [], null, null, - new Set([userId]), + new Set([adminUserId, normalUserId]), ) roomRepository.build(room) + + userRepository.create( + new User(adminUserId, true, false, roomId, adminUserIconId), + ) + userRepository.create( + new User(normalUserId, false, false, roomId, normalUserIconId), + ) }) test("正常系_管理者ユーザーがroomから退出する", async () => { - userRepository.create(new User(userId, true, false, roomId, iconId)) - - await userService.leaveRoom({ userId }) + await userService.leaveRoom({ userId: adminUserId }) - const user = await userRepository.find(userId) + const adminUser = await userRepository.find(adminUserId) - // roomから退出したuserは取得されない - expect(user).toBeNull() + // roomから退出したuserはfindで取得されない + expect(adminUser).toBeNull() }) test("正常系_一般ユーザーがroomから退出する", async () => { - userRepository.create(new User(userId, false, false, roomId, iconId)) - - await userService.leaveRoom({ userId }) + await userService.leaveRoom({ userId: normalUserId }) - const user = await userRepository.find(userId) + const normalUser = await userRepository.find(normalUserId) - // roomから退出したuserは取得されない - expect(user).toBeNull() + // roomから退出したuserはfindで取得されない + expect(normalUser).toBeNull() }) }) + + test("異常系_ roomに参加していないユーザーの場合、参加する前に通信が切断されてしまったと判断して何もしない", async () => { + await expect(() => userService.leaveRoom({ userId: uuid() })).resolves + }) }) diff --git a/app/server/src/domain/room/Room.ts b/app/server/src/domain/room/Room.ts index caa52726..5b4e9306 100644 --- a/app/server/src/domain/room/Room.ts +++ b/app/server/src/domain/room/Room.ts @@ -94,7 +94,15 @@ class RoomClass { return this._archivedAt } - public calcTimestamp = (topicId: number): number => { + public calcTimestamp = (topicId: number): number | null => { + const topic = this.findTopicOrThrow(topicId) + if (topic.state === "not-started") { + throw new Error(`Topic(id:${topicId}) was not started.`) + } + if (topic.state !== "ongoing") { + return null + } + const openedDate = this.findOpenedDateOrThrow(topicId) const offsetTime = this._topicTimeData[topicId].offsetTime const timestamp = new Date().getTime() - openedDate - offsetTime @@ -274,11 +282,17 @@ class RoomClass { * @returns MessageClass 運営botメッセージ */ private pauseTopic(topic: Topic): Message { - topic.state = "paused" - this._topicTimeData[topic.id].pausedDate = new Date().getTime() - return this.postBotMessage(topic.id, "【運営Bot】\n 発表が中断されました") + const message = this.postBotMessage( + topic.id, + "【運営Bot】\n 発表が中断されました", + ) + + // NOTE: stateの更新前に、botmessageのタイムスタンプを計算しておく必要がある + topic.state = "paused" + + return message } /** @@ -287,8 +301,6 @@ class RoomClass { * @returns MessageClass 運営botメッセージ */ private finishTopic = (topic: Topic): Message => { - topic.state = "finished" - // 質問の集計 const questions = this._chatItems.filter( (c): c is Question => c instanceof Question && c.topicId === topic.id, @@ -306,7 +318,7 @@ class RoomClass { ) // トピック終了のBotメッセージ - return this.postBotMessage( + const message = this.postBotMessage( topic.id, [ "【運営Bot】\n 発表が終了しました!\n(引き続きコメントを投稿いただけます)", @@ -316,6 +328,11 @@ class RoomClass { .filter(Boolean) .join("\n"), ) + + // NOTE: stateの更新前に、botmessageのタイムスタンプを計算しておく必要がある + topic.state = "finished" + + return message } /** @@ -344,12 +361,12 @@ class RoomClass { .filter( (chatItem): chatItem is Reaction => chatItem instanceof Reaction, ) - .find( + .some( ({ topicId, user, quote }) => topicId === chatItem.topicId && user.id === chatItem.user.id && quote.id === chatItem.quote.id, - ) != null + ) ) { throw new Error( `Reaction(topicId: ${chatItem.topicId}, user.id: ${chatItem.user.id}, quote.id: ${chatItem.quote.id}) has already exists.`, @@ -368,7 +385,7 @@ class RoomClass { content, null, new Date(), - this.calcTimestamp(topicId), + this.calcTimestamp(topicId) ?? undefined, ) this._chatItems.push(botMessage) diff --git a/app/server/src/domain/user/IconId.ts b/app/server/src/domain/user/IconId.ts index b48d6e1e..92375397 100644 --- a/app/server/src/domain/user/IconId.ts +++ b/app/server/src/domain/user/IconId.ts @@ -6,7 +6,7 @@ export const NewIconId = (value: number): IconId => { if (value < 0 || value > 11) { throw new Error(`value(${value}) is invalid for IconId: out of 0 to 10.`) } - return value as any + return value as unknown as IconId } export default IconId diff --git a/app/server/src/expressRoute.ts b/app/server/src/expressRoute.ts index 14a5403e..2ef47eb1 100644 --- a/app/server/src/expressRoute.ts +++ b/app/server/src/expressRoute.ts @@ -16,7 +16,7 @@ export type Routes = Omit & { req: express.Request< RestApi<"get", Path>["params"], RestApi<"get", Path>["response"], - RestApi<"get", Path>["request"], + unknown, RestApi<"get", Path>["query"] >, res: express.Response["response"]>, diff --git a/app/server/src/infra/repository/chatItem/ChatItemRepository.ts b/app/server/src/infra/repository/chatItem/ChatItemRepository.ts index 834254d9..2aaf6690 100644 --- a/app/server/src/infra/repository/chatItem/ChatItemRepository.ts +++ b/app/server/src/infra/repository/chatItem/ChatItemRepository.ts @@ -6,7 +6,6 @@ import Answer from "../../../domain/chatItem/Answer" import ChatItem from "../../../domain/chatItem/ChatItem" import PGPool from "../PGPool" import { ChatItemSenderType, ChatItemType } from "sushi-chat-shared" -import { formatDate } from "../../../utils/date" import User from "../../../domain/user/User" class ChatItemRepository implements IChatItemRepository { @@ -29,7 +28,7 @@ class ChatItemRepository implements IChatItemRepository { message.quote?.id, message.content, message.timestamp, - formatDate(message.createdAt), + message.createdAt, ]) } catch (e) { ChatItemRepository.logError(e, "saveMessage()") @@ -55,7 +54,7 @@ class ChatItemRepository implements IChatItemRepository { ChatItemRepository.senderTypeMap[reaction.senderType], reaction.quote.id, reaction.timestamp, - formatDate(reaction.createdAt), + reaction.createdAt, ]) } catch (e) { ChatItemRepository.logError(e, "saveReaction()") @@ -82,7 +81,7 @@ class ChatItemRepository implements IChatItemRepository { question.quote?.id, question.content, question.timestamp, - formatDate(question.createdAt), + question.createdAt, ]) } catch (e) { ChatItemRepository.logError(e, "saveQuestion()") @@ -109,7 +108,7 @@ class ChatItemRepository implements IChatItemRepository { (answer.quote as Question).id, answer.content, answer.timestamp, - formatDate(answer.createdAt), + answer.createdAt, ]) } catch (e) { ChatItemRepository.logError(e, "saveAnswer()") diff --git a/app/server/src/infra/repository/room/RoomRepository.ts b/app/server/src/infra/repository/room/RoomRepository.ts index d3e4733e..396d4380 100644 --- a/app/server/src/infra/repository/room/RoomRepository.ts +++ b/app/server/src/infra/repository/room/RoomRepository.ts @@ -7,7 +7,6 @@ import IStampRepository from "../../../domain/stamp/IStampRepository" import Topic, { TopicTimeData } from "../../../domain/room/Topic" import PGPool from "../PGPool" import { RoomState, TopicState } from "sushi-chat-shared" -import { formatDate } from "../../../utils/date" import IAdminRepository from "../../../domain/admin/IAdminRepository" import User from "../../../domain/user/User" @@ -181,7 +180,7 @@ class RoomRepository implements IRoomRepository { room.startAt, room.finishAt, room.archivedAt, - formatDate(new Date()), + new Date(), room.id, ]) } @@ -204,7 +203,7 @@ class RoomRepository implements IRoomRepository { pgClient.query(topicQuery, [ RoomRepository.topicStateMap[t.state], room.topicTimeData[t.id].offsetTime, - formatDate(new Date()), + new Date(), room.id, t.id, ]), diff --git a/app/server/src/infra/repository/stamp/StampRepository.ts b/app/server/src/infra/repository/stamp/StampRepository.ts index 6e29a18f..9381c312 100644 --- a/app/server/src/infra/repository/stamp/StampRepository.ts +++ b/app/server/src/infra/repository/stamp/StampRepository.ts @@ -9,7 +9,7 @@ class StampRepository implements IStampRepository { const pgClient = await this.pgPool.client() const query = - "INSERT INTO Stamps (id, room_id, topic_id, user_id, timestamp) VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO Stamps (id, room_id, topic_id, user_id, timestamp, created_at) VALUES ($1, $2, $3, $4, $5, $6)" try { await pgClient.query(query, [ @@ -18,6 +18,7 @@ class StampRepository implements IStampRepository { stamp.topicId, stamp.userId, stamp.timestamp, + stamp.createdAt, ]) } catch (e) { StampRepository.logError(e, "store()") diff --git a/app/server/src/ioServer.ts b/app/server/src/ioServer.ts index efdfb30c..ac01502e 100644 --- a/app/server/src/ioServer.ts +++ b/app/server/src/ioServer.ts @@ -171,9 +171,9 @@ const createSocketIOServer = async ( }) // トピック状態の変更 - socket.on("ADMIN_CHANGE_TOPIC_STATE", (received, callback) => { + socket.on("ADMIN_CHANGE_TOPIC_STATE", async (received, callback) => { try { - roomService.changeTopicState({ + await roomService.changeTopicState({ userId, topicId: received.topicId, state: received.state, @@ -241,9 +241,9 @@ const createSocketIOServer = async ( }) // スタンプを投稿する - socket.on("POST_STAMP", (received, callback) => { + socket.on("POST_STAMP", async (received, callback) => { try { - stampService.post({ + await stampService.post({ id: received.id, userId, topicId: received.topicId, @@ -254,9 +254,9 @@ const createSocketIOServer = async ( } }) - socket.on("POST_PINNED_MESSAGE", (received, callback) => { + socket.on("POST_PINNED_MESSAGE", async (received, callback) => { try { - chatItemService.pinChatItem({ chatItemId: received.chatItemId }) + await chatItemService.pinChatItem({ chatItemId: received.chatItemId }) callback({ result: "success", data: undefined }) } catch (e) { handleError(callback, "POST_PINNED_MESSAGE", e) @@ -265,9 +265,9 @@ const createSocketIOServer = async ( // ルームを終了する // eslint-disable-next-line @typescript-eslint/no-unused-vars - socket.on("ADMIN_FINISH_ROOM", (_, callback) => { + socket.on("ADMIN_FINISH_ROOM", async (_, callback) => { try { - roomService.finish({ userId: userId }) + await roomService.finish({ userId: userId }) callback({ result: "success", data: undefined }) } catch (e) { handleError(callback, "ADMIN_FINISH_ROOM", e) @@ -275,9 +275,9 @@ const createSocketIOServer = async ( }) //接続解除時に行う処理 - socket.on("disconnect", () => { + socket.on("disconnect", async () => { try { - userService.leaveRoom({ userId }) + await userService.leaveRoom({ userId }) } catch (e) { logError("disconnect", e) } diff --git a/app/server/src/rest.ts b/app/server/src/rest.ts index e1bdfb30..834889be 100644 --- a/app/server/src/rest.ts +++ b/app/server/src/rest.ts @@ -120,6 +120,7 @@ export const restSetup = ( adminRouter.get("/room", async (req, res) => { try { const rooms = await adminService.getManagedRooms({ + // @ts-ignore bodyをadminIdの受け渡しに利用しているため adminId: req.body.adminId, }) diff --git a/app/server/src/service/chatItem/ChatItemModelBuilder.ts b/app/server/src/service/chatItem/ChatItemModelBuilder.ts index 6d9dee38..16408a58 100644 --- a/app/server/src/service/chatItem/ChatItemModelBuilder.ts +++ b/app/server/src/service/chatItem/ChatItemModelBuilder.ts @@ -40,7 +40,7 @@ class ChatItemModelBuilder { iconId: message.user.iconId.valueOf(), content: message.content, quote: quoteModel, - timestamp: message.timestamp, + timestamp: message.timestamp ?? undefined, } } @@ -66,7 +66,7 @@ class ChatItemModelBuilder { senderType: reaction.senderType, iconId: reaction.user.iconId.valueOf(), quote: quoteModel, - timestamp: reaction.timestamp, + timestamp: reaction.timestamp ?? undefined, } } @@ -91,7 +91,7 @@ class ChatItemModelBuilder { iconId: question.user.iconId.valueOf(), content: question.content, quote: quoteModel, - timestamp: question.timestamp, + timestamp: question.timestamp ?? undefined, } } @@ -108,7 +108,7 @@ class ChatItemModelBuilder { quote: !recursive ? undefined : this.buildQuestion(answer.quote as Question), - timestamp: answer.timestamp, + timestamp: answer.timestamp ?? undefined, } } } diff --git a/app/server/src/service/chatItem/ChatItemService.ts b/app/server/src/service/chatItem/ChatItemService.ts index 07f17a3f..eea7f170 100644 --- a/app/server/src/service/chatItem/ChatItemService.ts +++ b/app/server/src/service/chatItem/ChatItemService.ts @@ -61,7 +61,7 @@ class ChatItemService { content, quote, new Date(), - room.calcTimestamp(topicId), + room.calcTimestamp(topicId) ?? undefined, ) room.postChatItem(userId, message) @@ -94,7 +94,7 @@ class ChatItemService { senderType, quote, new Date(), - room.calcTimestamp(topicId), + room.calcTimestamp(topicId) ?? undefined, ) room.postChatItem(userId, reaction) @@ -130,7 +130,7 @@ class ChatItemService { content, quote, new Date(), - room.calcTimestamp(topicId), + room.calcTimestamp(topicId) ?? undefined, ) room.postChatItem(userId, question) @@ -162,7 +162,7 @@ class ChatItemService { content, quote, new Date(), - room.calcTimestamp(topicId), + room.calcTimestamp(topicId) ?? undefined, ) room.postChatItem(userId, answer) @@ -177,6 +177,7 @@ class ChatItemService { chatItemId, this.chatItemRepository, ) + // TODO: speakerしかピン留めできないようにする this.chatItemDelivery.pinChatItem(pinnedChatItem) await this.chatItemRepository.pinChatItem(pinnedChatItem) diff --git a/app/server/src/service/stamp/StampService.ts b/app/server/src/service/stamp/StampService.ts index d3bdfb22..980e431b 100644 --- a/app/server/src/service/stamp/StampService.ts +++ b/app/server/src/service/stamp/StampService.ts @@ -24,7 +24,7 @@ class StampService { roomId, this.roomRepository, ) - const timestamp = room.calcTimestamp(topicId) + const timestamp = room.calcTimestamp(topicId) as number // NOTE: スタンプはongoing中しか送信できないため as number const stamp = this.stampFactory.create( id, diff --git a/app/server/src/utils/date.ts b/app/server/src/utils/date.ts deleted file mode 100644 index 06660e55..00000000 --- a/app/server/src/utils/date.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const formatDate = (date: Date) => - date.toISOString().replace(/T/, " ").replace(/\..+/, "") diff --git a/app/server/src/utils/range.ts b/app/server/src/utils/range.ts index ccfbf940..0bde4c61 100644 --- a/app/server/src/utils/range.ts +++ b/app/server/src/utils/range.ts @@ -1,7 +1,7 @@ /** - * [0, 1, 2, ..., n]という配列を作る関数 + * [0, 1, 2, ..., n-1]という配列を作る関数 * - * @param count 要素の数 - * @returns 配列 + * @param n 要素の数 + * @returns number[] */ -export const ArrayRange = (count: number) => [...Array(count)].map((_, i) => i) +export const ArrayRange = (n: number) => [...Array(n)].map((_, i) => i) diff --git a/app/shared/src/types/rest.ts b/app/shared/src/types/rest.ts index 66b8d3ed..7aef88bb 100644 --- a/app/shared/src/types/rest.ts +++ b/app/shared/src/types/rest.ts @@ -8,7 +8,6 @@ export type RestApiDefinition = { methods: { get: { query: Empty - request: Empty response: "ok" } } @@ -18,7 +17,6 @@ export type RestApiDefinition = { methods: { get: { query: Empty - request: Empty response: SuccessResponse | ErrorResponse } post: { @@ -41,7 +39,6 @@ export type RestApiDefinition = { methods: { get: { query: Empty - request: Empty response: SuccessResponse | ErrorResponse } } @@ -65,7 +62,6 @@ export type RestApiDefinition = { methods: { get: { query: Empty - request: Empty response: | SuccessResponse<{ chatItems: ChatItemModel[] @@ -109,12 +105,16 @@ export type GeneralRestApiTypes = { [path: string]: { params: Record methods: { - [K in "get" | "post" | "put"]?: { + [K in "post" | "put"]?: { query: Record request: unknown response: unknown } } + get?: { + query: Record + response: unknown + } } } @@ -144,7 +144,6 @@ export type RestApi< > = Method extends "get" ? Path extends GetMethodPath ? { - request: RestApiDefinition[Path]["methods"]["get"]["request"] response: RestApiDefinition[Path]["methods"]["get"]["response"] params: RestApiDefinition[Path]["params"] query: RestApiDefinition[Path]["methods"]["get"]["query"]