From cd301a795d8e8d78ee6f312e86bd79fe2f22e377 Mon Sep 17 00:00:00 2001 From: luizy Date: Sun, 26 Nov 2023 23:25:03 +0900 Subject: [PATCH 001/378] feat: Guards add - SessionGuard add - CommandGuard add [#125] --- packages/backend/src/command.guard.ts | 10 ++++++++ .../backend/src/quizzes/quizzes.controller.ts | 25 ++++++++----------- .../backend/src/session/session.decorator.ts | 8 ++++++ packages/backend/src/session/session.guard.ts | 16 ++++++++++++ 4 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 packages/backend/src/command.guard.ts create mode 100644 packages/backend/src/session/session.decorator.ts create mode 100644 packages/backend/src/session/session.guard.ts diff --git a/packages/backend/src/command.guard.ts b/packages/backend/src/command.guard.ts new file mode 100644 index 0000000..97f86be --- /dev/null +++ b/packages/backend/src/command.guard.ts @@ -0,0 +1,10 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +@Injectable() +export class CommandGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const command = request.body['command']; + return typeof command === 'string' && command.startsWith('git'); + } +} diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index 08aa6a9..783f757 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -7,9 +7,9 @@ import { HttpException, HttpStatus, Res, - Req, Inject, Delete, + UseGuards, } from '@nestjs/common'; import { ApiTags, @@ -25,8 +25,11 @@ import { QuizzesDto } from './dto/quizzes.dto'; import { CommandRequestDto } from './dto/command-request.dto'; import { CommandResponseDto } from './dto/command-response.dto'; import { SessionService } from '../session/session.service'; -import { Request, Response } from 'express'; +import { Response } from 'express'; import { ContainersService } from '../containers/containers.service'; +import { SessionId } from '../session/session.decorator'; +import { SessionGuard } from '../session/session.guard'; +import { CommandGuard } from '../command.guard'; @ApiTags('quizzes') @Controller('api/v1/quizzes') @@ -66,6 +69,7 @@ export class QuizzesController { } @Post(':id/command') + @UseGuards(CommandGuard) @ApiOperation({ summary: 'Git 명령을 실행합니다.' }) @ApiResponse({ status: 200, @@ -78,22 +82,20 @@ export class QuizzesController { @Param('id') id: number, @Body() execCommandDto: CommandRequestDto, @Res() response: Response, - @Req() request: Request, + @SessionId() sessionId: string, ): Promise { try { - let sessionId = request.cookies?.sessionId; - if (!sessionId) { // 세션 아이디가 없다면 + this.logger.log('info', 'no session id. creating session..'); response.cookie( 'sessionId', (sessionId = await this.sessionService.createSession()), { httpOnly: true, - // 개발 이후 활성화 시켜야함 - // secure: true, }, ); // 세션 아이디를 생성한다. + this.logger.log('info', `session id: ${sessionId} created`); } let containerId = await this.sessionService.getContainerIdBySessionId( @@ -148,6 +150,7 @@ export class QuizzesController { } @Delete(':id/command') + @UseGuards(SessionGuard) @ApiOperation({ summary: 'Git 명령기록과, 할당된 컨테이너를 삭제합니다' }) @ApiResponse({ status: 200, @@ -157,15 +160,9 @@ export class QuizzesController { @ApiParam({ name: 'id', description: '문제 ID' }) async deleteCommandHistory( @Param('id') id: number, - @Req() request: Request, + @SessionId() sessionId: string, ): Promise { try { - const sessionId = request.cookies?.sessionId; - - if (!sessionId) { - return; - } - const containerId = await this.sessionService.getContainerIdBySessionId( sessionId, id, diff --git a/packages/backend/src/session/session.decorator.ts b/packages/backend/src/session/session.decorator.ts new file mode 100644 index 0000000..f23d275 --- /dev/null +++ b/packages/backend/src/session/session.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const SessionId = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.cookies['sessionId']; + }, +); diff --git a/packages/backend/src/session/session.guard.ts b/packages/backend/src/session/session.guard.ts new file mode 100644 index 0000000..1cd47e8 --- /dev/null +++ b/packages/backend/src/session/session.guard.ts @@ -0,0 +1,16 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +/** + * @description session guard + * @returns {boolean} + * check if sessionId exists + * @dependency cookie-parser + */ +@Injectable() +export class SessionGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + // cookie-parser must be used before this guard + return request['cookies'].sessionId; + } +} From ee40e1e4eb9d71a61f6095fde9b4cba6dc60c815 Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 00:38:38 +0900 Subject: [PATCH 002/378] =?UTF-8?q?style:=20a=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- packages/frontend/src/design-system/styles/reset.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/design-system/styles/reset.css b/packages/frontend/src/design-system/styles/reset.css index 61972a0..da7eaab 100644 --- a/packages/frontend/src/design-system/styles/reset.css +++ b/packages/frontend/src/design-system/styles/reset.css @@ -14,7 +14,10 @@ h6, p, blockquote, pre, -a, +a { + color: inherit; + text-decoration: none; +} abbr, acronym, address, From 903930f395b8c0ecfa6747665bf5a33e7a422ed8 Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 00:38:56 +0900 Subject: [PATCH 003/378] =?UTF-8?q?chore:=20BadgeGroup=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- .../components/common/Badge/BadgeGroup.tsx | 23 ------------------- .../components/common/Badge/index.ts | 1 - .../design-system/components/common/index.ts | 2 +- 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 packages/frontend/src/design-system/components/common/Badge/BadgeGroup.tsx diff --git a/packages/frontend/src/design-system/components/common/Badge/BadgeGroup.tsx b/packages/frontend/src/design-system/components/common/Badge/BadgeGroup.tsx deleted file mode 100644 index 65cf316..0000000 --- a/packages/frontend/src/design-system/components/common/Badge/BadgeGroup.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { objectKeys } from "../../../../utils/types"; - -import { Badge } from "./Badge"; -import * as styles from "./Badge.css"; - -const variants = objectKeys(styles.badgeVariants); -export interface BadgeGroupProps { - items: { label: string }[]; -} - -export function BadgeGroup({ items }: BadgeGroupProps) { - return ( -
- {items.map((item, index) => ( - - ))} -
- ); -} diff --git a/packages/frontend/src/design-system/components/common/Badge/index.ts b/packages/frontend/src/design-system/components/common/Badge/index.ts index 94a4849..5c70427 100644 --- a/packages/frontend/src/design-system/components/common/Badge/index.ts +++ b/packages/frontend/src/design-system/components/common/Badge/index.ts @@ -1,2 +1 @@ export { Badge } from "./Badge"; -export { BadgeGroup } from "./BadgeGroup"; diff --git a/packages/frontend/src/design-system/components/common/index.ts b/packages/frontend/src/design-system/components/common/index.ts index 886886e..5dc442d 100644 --- a/packages/frontend/src/design-system/components/common/index.ts +++ b/packages/frontend/src/design-system/components/common/index.ts @@ -2,7 +2,7 @@ export { default as Header } from "./Header"; export { default as Button } from "./Button"; export { default as Modal } from "./Modal"; export { default as SideBar } from "./SideBar"; -export { Badge, BadgeGroup } from "./Badge"; +export { Badge } from "./Badge"; export { toast, ToastContainer } from "./Toast"; export { Accordion, useAccordion } from "./Accordion"; export { default as Footer } from "./Footer"; From 014e0124c2abbcda5c7af57b542af0d6b8a51b98 Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 00:39:23 +0900 Subject: [PATCH 004/378] =?UTF-8?q?feat:=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EC=95=84=EC=BD=94=EB=94=94=EC=96=B8=EC=97=90=20git-book=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- .../CommandAccordion/CommandAccordion.css.ts | 12 ++++++--- .../CommandAccordion/CommandAccordion.tsx | 25 +++++++++++-------- .../components/common/Badge/Badge.css.ts | 5 ---- .../components/common/Badge/Badge.tsx | 6 ++--- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.css.ts b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.css.ts index 51a3412..677680a 100644 --- a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.css.ts +++ b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.css.ts @@ -1,7 +1,13 @@ import { style } from "@vanilla-extract/css"; -const badgeGroupLayout = style({ - marginTop: "6px", -}); +import { flex } from "../../../design-system/tokens/utils.css"; + +const badgeGroupLayout = style([ + flex, + { + marginTop: "6px", + gap: 10, + }, +]); export default badgeGroupLayout; diff --git a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx index a7b84cb..c704379 100644 --- a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx +++ b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx @@ -1,7 +1,8 @@ -import { - Accordion, - BadgeGroup, -} from "../../../design-system/components/common"; +import Link from "next/link"; + +import { Accordion, Badge } from "../../../design-system/components/common"; +import { badgeVariants } from "../../../design-system/components/common/Badge/Badge.css"; +import { objectKeys } from "../../../utils/types"; import badgeGroupLayout from "./CommandAccordion.css"; @@ -10,10 +11,12 @@ interface CommandAccordionProps { items: string[]; } + export default function CommandAccordion({ width = "100%", - items, -}: CommandAccordionProps) { + items, +}: CommandAccordionProps) { const variants = objectKeys(badgeVariants); + const gitBookURL = "https://git-scm.com/docs/git"; return ( @@ -21,13 +24,13 @@ export default function CommandAccordion({ {({ open }) => <>핵심명령어 {open ? "숨기기" : "보기"}}
- + {items.map((item, index) => ( + + {item} + + ))}
); } - -function toLabelProps(item: string) { - return { label: item }; -} diff --git a/packages/frontend/src/design-system/components/common/Badge/Badge.css.ts b/packages/frontend/src/design-system/components/common/Badge/Badge.css.ts index 34407df..7f5f86d 100644 --- a/packages/frontend/src/design-system/components/common/Badge/Badge.css.ts +++ b/packages/frontend/src/design-system/components/common/Badge/Badge.css.ts @@ -3,11 +3,6 @@ import { style, styleVariants } from "@vanilla-extract/css"; import color from "../../../tokens/color"; import typography from "../../../tokens/typography"; -export const container = style({ - display: "flex", - gap: 10, -}); - export const badgeBase = style([ typography.$semantic.caption2Regular, { diff --git a/packages/frontend/src/design-system/components/common/Badge/Badge.tsx b/packages/frontend/src/design-system/components/common/Badge/Badge.tsx index 20d91e9..58b2192 100644 --- a/packages/frontend/src/design-system/components/common/Badge/Badge.tsx +++ b/packages/frontend/src/design-system/components/common/Badge/Badge.tsx @@ -9,13 +9,13 @@ export type BadgeVariantType = keyof typeof badgeVariants; export interface BadgeProps { variant: BadgeVariantType; - label: ReactNode; + children: ReactNode; } -export function Badge({ variant, label }: BadgeProps) { +export function Badge({ variant, children }: BadgeProps) { const badgeStyle = classnames( styles.badgeBase, styles.badgeVariants[variant], ); - return {label}; + return {children}; } From 9cead7b49e7e553fc6678142e903b52b56b27f4f Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 00:50:28 +0900 Subject: [PATCH 005/378] =?UTF-8?q?refactor:=20git-book=20url=20=EC=83=81?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- packages/frontend/src/constants/path.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/constants/path.ts b/packages/frontend/src/constants/path.ts index 49cdb38..367e1e7 100644 --- a/packages/frontend/src/constants/path.ts +++ b/packages/frontend/src/constants/path.ts @@ -8,4 +8,6 @@ const API_PATH = { QUIZZES: "/quizzes", } as const; -export { BROWSWER_PATH, API_PATH }; +const GIT_BOOK_URL = "https://git-scm.com/docs/git"; + +export { BROWSWER_PATH, API_PATH, GIT_BOOK_URL }; From 97618321e7f79d6d6f62afc57b60488505d1d13a Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 00:51:22 +0900 Subject: [PATCH 006/378] =?UTF-8?q?refactor:=20BadgeVariantList=20?= =?UTF-8?q?=EB=B1=83=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- .../quiz/CommandAccordion/CommandAccordion.tsx | 15 +++++++-------- .../components/common/Badge/Badge.tsx | 3 +++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx index c704379..29136de 100644 --- a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx +++ b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; +import { GIT_BOOK_URL } from "../../../constants/path"; import { Accordion, Badge } from "../../../design-system/components/common"; -import { badgeVariants } from "../../../design-system/components/common/Badge/Badge.css"; -import { objectKeys } from "../../../utils/types"; +import { badgeVariantList } from "../../../design-system/components/common/Badge/Badge"; import badgeGroupLayout from "./CommandAccordion.css"; @@ -14,9 +14,8 @@ interface CommandAccordionProps { export default function CommandAccordion({ width = "100%", - items, -}: CommandAccordionProps) { const variants = objectKeys(badgeVariants); - const gitBookURL = "https://git-scm.com/docs/git"; + items, +}: CommandAccordionProps) { return ( @@ -25,12 +24,12 @@ export default function CommandAccordion({
{items.map((item, index) => ( - - {item} + + {item} ))}
); -} +} \ No newline at end of file diff --git a/packages/frontend/src/design-system/components/common/Badge/Badge.tsx b/packages/frontend/src/design-system/components/common/Badge/Badge.tsx index 58b2192..675c711 100644 --- a/packages/frontend/src/design-system/components/common/Badge/Badge.tsx +++ b/packages/frontend/src/design-system/components/common/Badge/Badge.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import classnames from "../../../../utils/classnames"; +import { objectKeys } from "../../../../utils/types"; import { badgeVariants } from "./Badge.css"; import * as styles from "./Badge.css"; @@ -12,6 +13,8 @@ export interface BadgeProps { children: ReactNode; } +export const badgeVariantList = objectKeys(badgeVariants); + export function Badge({ variant, children }: BadgeProps) { const badgeStyle = classnames( styles.badgeBase, From 4c4583b9b4f7a5fb3352855fd575a79b21b7afc9 Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 10:54:30 +0900 Subject: [PATCH 007/378] =?UTF-8?q?refactor:=20badgeVariantList=20common?= =?UTF-8?q?=20=ED=8F=B4=EB=8D=94=20index=EC=97=90=EC=84=9C=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#17] --- .../components/quiz/CommandAccordion/CommandAccordion.tsx | 6 ++---- .../src/design-system/components/common/Badge/index.ts | 2 +- .../frontend/src/design-system/components/common/index.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx index 29136de..9945d65 100644 --- a/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx +++ b/packages/frontend/src/components/quiz/CommandAccordion/CommandAccordion.tsx @@ -1,8 +1,7 @@ import Link from "next/link"; import { GIT_BOOK_URL } from "../../../constants/path"; -import { Accordion, Badge } from "../../../design-system/components/common"; -import { badgeVariantList } from "../../../design-system/components/common/Badge/Badge"; +import { Accordion, Badge , badgeVariantList } from "../../../design-system/components/common"; import badgeGroupLayout from "./CommandAccordion.css"; @@ -11,7 +10,6 @@ interface CommandAccordionProps { items: string[]; } - export default function CommandAccordion({ width = "100%", items, @@ -32,4 +30,4 @@ export default function CommandAccordion({ ); -} \ No newline at end of file +} diff --git a/packages/frontend/src/design-system/components/common/Badge/index.ts b/packages/frontend/src/design-system/components/common/Badge/index.ts index 5c70427..147c806 100644 --- a/packages/frontend/src/design-system/components/common/Badge/index.ts +++ b/packages/frontend/src/design-system/components/common/Badge/index.ts @@ -1 +1 @@ -export { Badge } from "./Badge"; +export { Badge, badgeVariantList } from "./Badge"; diff --git a/packages/frontend/src/design-system/components/common/index.ts b/packages/frontend/src/design-system/components/common/index.ts index 5dc442d..3ada564 100644 --- a/packages/frontend/src/design-system/components/common/index.ts +++ b/packages/frontend/src/design-system/components/common/index.ts @@ -2,7 +2,7 @@ export { default as Header } from "./Header"; export { default as Button } from "./Button"; export { default as Modal } from "./Modal"; export { default as SideBar } from "./SideBar"; -export { Badge } from "./Badge"; +export { Badge, badgeVariantList } from "./Badge"; export { toast, ToastContainer } from "./Toast"; export { Accordion, useAccordion } from "./Accordion"; export { default as Footer } from "./Footer"; From 4798db62b912c8a37db90bdc3a520b38817a3667 Mon Sep 17 00:00:00 2001 From: CHAE Date: Mon, 27 Nov 2023 23:27:21 +0900 Subject: [PATCH 008/378] =?UTF-8?q?fix:=20=ED=80=B4=EC=A6=88=20=EB=B0=94?= =?UTF-8?q?=EB=80=94=20=EB=95=8C=20=EB=A7=88=EB=8B=A4=20=ED=84=B0=EB=AF=B8?= =?UTF-8?q?=EB=84=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#132] --- packages/frontend/src/pages/quizzes/[id].tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/quizzes/[id].tsx b/packages/frontend/src/pages/quizzes/[id].tsx index 501105c..68e345b 100644 --- a/packages/frontend/src/pages/quizzes/[id].tsx +++ b/packages/frontend/src/pages/quizzes/[id].tsx @@ -1,7 +1,7 @@ import axios from "axios"; import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from "next"; import { useRouter } from "next/router"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { quizAPI } from "../../apis/quizzes"; import * as styles from "../../components/demo/Demo.css"; @@ -19,6 +19,10 @@ export default function QuizPage({ quiz }: { quiz: Quiz }) { query: { id }, } = useRouter(); + useEffect(() => { + setContentArray([]); + }, [id]); + const handleTerminal = async (input: string) => { if (!isString(id)) { return; From 1b14e61b5064e9411f33595eaa638ce099a5bc57 Mon Sep 17 00:00:00 2001 From: CHAE Date: Mon, 27 Nov 2023 23:32:19 +0900 Subject: [PATCH 009/378] =?UTF-8?q?style:=20=ED=84=B0=EB=AF=B8=EB=84=90=20?= =?UTF-8?q?input=EC=97=90=20padding=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#132] --- packages/frontend/src/components/terminal/Terminal.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/terminal/Terminal.css.ts b/packages/frontend/src/components/terminal/Terminal.css.ts index 394bc73..95c0222 100644 --- a/packages/frontend/src/components/terminal/Terminal.css.ts +++ b/packages/frontend/src/components/terminal/Terminal.css.ts @@ -40,6 +40,7 @@ export const commandInputContainer = style([ { width: "100%", position: "relative", + paddingLeft: 6, }, ]); From 8d1b405eda44e80b60de72a42a0b6bbff31b9629 Mon Sep 17 00:00:00 2001 From: CHAE Date: Tue, 28 Nov 2023 11:21:47 +0900 Subject: [PATCH 010/378] =?UTF-8?q?refactor:=20padding=20text-indent?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#132] --- packages/frontend/src/components/terminal/Terminal.css.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/frontend/src/components/terminal/Terminal.css.ts b/packages/frontend/src/components/terminal/Terminal.css.ts index 95c0222..0111a39 100644 --- a/packages/frontend/src/components/terminal/Terminal.css.ts +++ b/packages/frontend/src/components/terminal/Terminal.css.ts @@ -40,7 +40,6 @@ export const commandInputContainer = style([ { width: "100%", position: "relative", - paddingLeft: 6, }, ]); @@ -48,14 +47,13 @@ export const prompt = style({ position: "absolute", top: 1, left: 0, - paddingRight: 4, }); export const stdinContainer = style({ position: "relative" }); export const stdin = style({ display: "block", - textIndent: 10, + textIndent: 16, }); export const commandInput = style({ From 472c2bcd5956562950e491b6e598bb55f3476ac9 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Mon, 27 Nov 2023 20:13:51 +0900 Subject: [PATCH 011/378] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env에 CONTAINER_GIT_USERNAME 추가됐습니다! - restricted-shell 을 이용하도록 강제합니다. 나중에 보안에 더 신경을... - 컨테이너 이미지 버전 업(0.1 -> 0.2)에 따라 버전 업데이트 했습니다. - 실행 디렉토리가 변경되어서 반영했습니다. [#134] --- .../backend/src/containers/containers.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index 6c168aa..0aeae7b 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -63,7 +63,7 @@ export class ContainersService { command: string, ): Promise { const { stdoutData, stderrData } = await this.executeSSHCommand( - `docker exec -w ~/quiz/ ${container} ${command}`, + `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/restricted-shell ${command}`, ); if (stderrData) { @@ -78,20 +78,20 @@ export class ContainersService { // 차후에는 준비된 컨테이너 중 하나를 선택해서 준다. // quizId에 대한 유효성 검사는 이미 끝났다(이미 여기서는 DB 접근 불가) - const host: string = this.configService.get( - 'CONTAINER_SSH_USERNAME', + const user: string = this.configService.get( + 'CONTAINER_GIT_USERNAME', ); - const createContainerCommand = `docker run --network none -itd mergemasters/alpine-git:0.1 /bin/sh`; + const createContainerCommand = `docker run --network none -itd mergemasters/alpine-git:0.2 /bin/sh`; const { stdoutData } = await this.executeSSHCommand(createContainerCommand); const containerId = stdoutData.trim(); - const createDirectoryCommand = `docker exec ${containerId} mkdir -p /${host}/quiz/`; - await this.executeSSHCommand(createDirectoryCommand); - - const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/${host}/quiz/`; + const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`; await this.executeSSHCommand(copyFilesCommand); + const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}/quiz`; + await this.executeSSHCommand(chownCommand); + return containerId; } From ff273fc042eaee50a3f60328760af29d1902ff37 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Mon, 27 Nov 2023 20:15:40 +0900 Subject: [PATCH 012/378] =?UTF-8?q?config:=20CONTAINER=5FGIT=5FHOST=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#134] --- .github/workflows/backend-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend-deploy.yml b/.github/workflows/backend-deploy.yml index da95be6..097ad9f 100644 --- a/.github/workflows/backend-deploy.yml +++ b/.github/workflows/backend-deploy.yml @@ -74,5 +74,6 @@ jobs: -e CONTAINER_SSH_PORT=${{ secrets.CONTAINER_SSH_PORT }} \ -e CONTAINER_SSH_USERNAME=${{ secrets.CONTAINER_SSH_USERNAME }} \ -e CONTAINER_SSH_PASSWORD=${{ secrets.CONTAINER_SSH_PASSWORD }} \ + -e CONTAINER_GIT_HOST=${{ secrets.CONTAINER_GIT_HOST }} \ -e MONGODB_HOST=${{ secrets.MONGODB_HOST }} \ ${{ secrets.DOCKERHUB_USERNAME }}/git-challenge-backend:0.1 From 2182583716c99799b23c958e3fbcd100cc4b5d0d Mon Sep 17 00:00:00 2001 From: flydog98 Date: Mon, 27 Nov 2023 21:18:16 +0900 Subject: [PATCH 013/378] =?UTF-8?q?feat:=20requestDTO=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=94=EC=96=B4=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20swagger=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#72] --- .../src/quizzes/dto/command-request.dto.ts | 15 +++++++++++++-- .../backend/src/quizzes/quizzes.controller.ts | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/quizzes/dto/command-request.dto.ts b/packages/backend/src/quizzes/dto/command-request.dto.ts index 6efdc74..703b3cc 100644 --- a/packages/backend/src/quizzes/dto/command-request.dto.ts +++ b/packages/backend/src/quizzes/dto/command-request.dto.ts @@ -1,6 +1,17 @@ import { ApiProperty } from '@nestjs/swagger'; export class CommandRequestDto { - @ApiProperty({ description: '실행할 명령문', example: 'git branch' }) - command: string; + @ApiProperty({ + description: + '실행할 명령 모드. 예: "command" (명령 실행), "editor" (에디터 명령)', + example: 'command', + }) + mode: string; + + @ApiProperty({ + description: + '실행할 명령문 or 에디터 작성 본문. 예: "git status (명령 실행), "feat: tmp.js 파일 제거" (에디터 명령)', + example: 'git status', + }) + message: string; } diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index 783f757..3af4cde 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -118,16 +118,16 @@ export class QuizzesController { this.logger.log( 'info', - `running command "${execCommandDto.command}" for container ${containerId}`, + `running command "${execCommandDto.message}" for container ${containerId}`, ); const { message, result } = await this.containerService.runGitCommand( containerId, - execCommandDto.command, + execCommandDto.message, ); this.sessionService.pushLogBySessionId( - execCommandDto.command, + execCommandDto.message, sessionId, id, ); From 53417bea76f83894c33b106f0f25100e03ccf581 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:03:30 +0900 Subject: [PATCH 014/378] =?UTF-8?q?config:=20editor=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=20=EC=8B=9C=20message=20escaping=ED=95=98=EB=8A=94=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#72] --- packages/backend/package.json | 2 ++ yarn.lock | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/backend/package.json b/packages/backend/package.json index 93563cd..b0900ee 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -41,6 +41,7 @@ "papaparse": "^5.4.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "shell-escape": "^0.2.0", "sqlite3": "^5.1.6", "ssh2": "^1.14.0", "typeorm": "^0.3.17", @@ -56,6 +57,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/papaparse": "^5", + "@types/shell-escape": "^0.2.3", "@types/ssh2": "^1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index b223cae..291337a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5444,6 +5444,13 @@ __metadata: languageName: node linkType: hard +"@types/shell-escape@npm:^0.2.3": + version: 0.2.3 + resolution: "@types/shell-escape@npm:0.2.3" + checksum: 4f153f424138339fff4be34a8316402b8866cbf94cad40667affacc50547f85a702554c99c9c3d25a69434c2911312e86d4ea37616a216a9074d39c24221559c + languageName: node + linkType: hard + "@types/ssh2@npm:^1": version: 1.11.17 resolution: "@types/ssh2@npm:1.11.17" @@ -6872,6 +6879,7 @@ __metadata: "@types/jest": "npm:^29.5.2" "@types/node": "npm:^20.3.1" "@types/papaparse": "npm:^5" + "@types/shell-escape": "npm:^0.2.3" "@types/ssh2": "npm:^1" "@types/supertest": "npm:^2.0.12" "@typescript-eslint/eslint-plugin": "npm:^6.0.0" @@ -6890,6 +6898,7 @@ __metadata: prettier: "npm:^3.1.0" reflect-metadata: "npm:^0.1.13" rxjs: "npm:^7.8.1" + shell-escape: "npm:^0.2.0" source-map-support: "npm:^0.5.21" sqlite3: "npm:^5.1.6" ssh2: "npm:^1.14.0" @@ -16209,6 +16218,13 @@ __metadata: languageName: node linkType: hard +"shell-escape@npm:^0.2.0": + version: 0.2.0 + resolution: "shell-escape@npm:0.2.0" + checksum: 501616713d13fd053b3858da18b613d83bfcdc8368e62be393dc563cc9fe2550492d403f73211e9a84429f39c8b9617f1e016dabee177b0ebdcb298fa47fc612 + languageName: node + linkType: hard + "shelljs@npm:0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" From 875367c82aeb00d6c0499a564b34f52b2841e99e Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:04:53 +0900 Subject: [PATCH 015/378] =?UTF-8?q?feat:=20dto=EC=97=90=20as=20const?= =?UTF-8?q?=EC=99=80=20Type=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 여전히 TS를 잘 몰라서... 이렇게 하는게 맞겠죠? [#72] --- .../backend/src/quizzes/dto/command-request.dto.ts | 10 +++++++++- .../backend/src/quizzes/dto/command-response.dto.ts | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/quizzes/dto/command-request.dto.ts b/packages/backend/src/quizzes/dto/command-request.dto.ts index 703b3cc..8018439 100644 --- a/packages/backend/src/quizzes/dto/command-request.dto.ts +++ b/packages/backend/src/quizzes/dto/command-request.dto.ts @@ -1,12 +1,20 @@ import { ApiProperty } from '@nestjs/swagger'; +export const MODE = { + COMMAND: 'command', + EDITOR: 'editor', +} as const; + +type ModeType = (typeof MODE)[keyof typeof MODE]; + export class CommandRequestDto { @ApiProperty({ description: '실행할 명령 모드. 예: "command" (명령 실행), "editor" (에디터 명령)', example: 'command', + enum: Object.values(MODE), }) - mode: string; + mode: ModeType; @ApiProperty({ description: diff --git a/packages/backend/src/quizzes/dto/command-response.dto.ts b/packages/backend/src/quizzes/dto/command-response.dto.ts index a8c35aa..21002ce 100644 --- a/packages/backend/src/quizzes/dto/command-response.dto.ts +++ b/packages/backend/src/quizzes/dto/command-response.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; +export const RESULT = { + SUCCESS: 'success', + FAIL: 'fail', + EDITOR: 'editor', +} as const; + +type ResultType = (typeof RESULT)[keyof typeof RESULT]; + export class CommandResponseDto { @ApiProperty({ description: '실행한 stdout/stderr 결과', @@ -8,10 +16,10 @@ export class CommandResponseDto { message: string; @ApiProperty({ - description: '실행 결과 요약(stdout => success, stderr => fail, vi)', + description: `실행 결과 요약(stdout => "success", stderr => "fail", 에디터 사용 => "editor")`, example: 'success', }) - result: 'success' | 'fail' | 'vi'; + result: ResultType; @ApiProperty({ description: 'git 그래프 상황(아직 미구현)', From c75f2c190b10285ade6f0c43d27b7ad5989c36b2 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:05:29 +0900 Subject: [PATCH 016/378] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20=EC=84=B8=EC=85=98=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git commit 등 editor를 사용하는 명령에서 가장 최근 명령을 확인하기 위해 최근 로그를 조회하는 함수를 작성했습니다. [#72] --- packages/backend/src/session/session.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index 7f78be1..06ab141 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -71,6 +71,17 @@ export class SessionService { session.save(); } + async getRecentLog(sessionId: string, problemId: number): Promise { + const session = await this.getSessionById(sessionId); + + const problemLogs = session?.problems.get(problemId)?.logs; + if (!problemLogs || problemLogs.length === 0) { + throw new Error('No execution record(이전에 명령을 실행한 적 없습니다.)'); + } + + return problemLogs[problemLogs.length - 1]; + } + async pushLogBySessionId( command: string, sessionId: string, From 26ee44256ec6655051eeb79edef49e75aca4b699 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:06:21 +0900 Subject: [PATCH 017/378] =?UTF-8?q?feat:=20editor=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20editor=20=EB=AA=85=EB=A0=B9=20=EC=88=98?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설계한 대로 input.sh로 core.editor를 바꾸고, 명령을 실행한 뒤, output.sh로 돌려 놓습니다. - CREATED_BY_OUTPUT.SH 로 끝나는 경우 editor를 리턴하는 로직을 작성했습니다. - getContainer 내부에 editor를 마운팅하는 로직, editor 설정 로직도 추가됐습니다. - getContainer 내부에서 quizzer 권한을 일단 /home/quizzer/quiz에서 /home/quizzer로 확장했습니다. (git config때문) [#72] --- .../src/containers/containers.service.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index 0aeae7b..0edc347 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { Logger } from 'winston'; import { CommandResponseDto } from 'src/quizzes/dto/command-response.dto'; import { Client } from 'ssh2'; +import shellEscape from 'shell-escape'; @Injectable() export class ContainersService { @@ -66,6 +67,28 @@ export class ContainersService { `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/restricted-shell ${command}`, ); + if (stdoutData.endsWith('# CREATED_BY_OUTPUT.SH\n')) { + return { message: stdoutData, result: 'editor' }; + } + + if (stderrData) { + return { message: stderrData, result: 'fail' }; + } + + return { message: stdoutData, result: 'success' }; + } + + async runEditorCommand( + container: string, + command: string, + message: string, + ): Promise { + const escapedMessage = shellEscape([message]); + + const { stdoutData, stderrData } = await this.executeSSHCommand( + `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} sh -c "git config --global core.editor /editor/input.sh && echo ${escapedMessage} | ${command}; git config --global core.editor /editor/output.sh"`, + ); + if (stderrData) { return { message: stderrData, result: 'fail' }; } @@ -82,16 +105,21 @@ export class ContainersService { 'CONTAINER_GIT_USERNAME', ); - const createContainerCommand = `docker run --network none -itd mergemasters/alpine-git:0.2 /bin/sh`; + const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ +mergemasters/alpine-git:0.2 /bin/sh`; const { stdoutData } = await this.executeSSHCommand(createContainerCommand); const containerId = stdoutData.trim(); + // TODO: 연속 실행할 때 매번 SSH 하는거 리팩토링 해야 함 const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`; await this.executeSSHCommand(copyFilesCommand); - const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}/quiz`; + const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}`; await this.executeSSHCommand(chownCommand); + const coreEditorCommand = `docker exec -w /home/quizzer/quiz/ -u ${user} ${containerId} git config --global core.editor /editor/output.sh`; + await this.executeSSHCommand(coreEditorCommand); + return containerId; } From e0f388fa95cdb7de4c7321fd3b040c53d613f16f Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:14:41 +0900 Subject: [PATCH 018/378] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=94=EC=96=B4=20Guard=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#72] --- packages/backend/src/command.guard.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/command.guard.ts b/packages/backend/src/command.guard.ts index 97f86be..c6a6c1a 100644 --- a/packages/backend/src/command.guard.ts +++ b/packages/backend/src/command.guard.ts @@ -4,7 +4,12 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; export class CommandGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); - const command = request.body['command']; - return typeof command === 'string' && command.startsWith('git'); + const mode = request.body['mode']; + const message = request.body['message']; + return ( + typeof mode === 'string' && + typeof message === 'string' && + (mode === 'editor' || (mode === 'command' && message.startsWith('git'))) + ); } } From 570b240964389b8771a7d9b1c136790bd48ce3ee Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 00:15:02 +0900 Subject: [PATCH 019/378] =?UTF-8?q?feat:=20mode=20=3D=3D=3D=20EDITOR=20?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EA=B8=B0=ED=95=98=EC=97=AC=20editor=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드가 매우 더럽습니다! 일부러 일관성 있게 유지했어요. 어떻게 개선하면 좋을지 같이 얘기해봐요~! - 로그 역시 schema 변경이 불가피할 것 같습니다. 일단 message라면 무조건 로깅하고 있습니다.(editor일때도!) [#72] --- .../backend/src/quizzes/quizzes.controller.ts | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index 3af4cde..483b698 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -22,7 +22,7 @@ import { Logger } from 'winston'; import { QuizDto } from './dto/quiz.dto'; import { QuizzesService } from './quizzes.service'; import { QuizzesDto } from './dto/quizzes.dto'; -import { CommandRequestDto } from './dto/command-request.dto'; +import { CommandRequestDto, MODE } from './dto/command-request.dto'; import { CommandResponseDto } from './dto/command-response.dto'; import { SessionService } from '../session/session.service'; import { Response } from 'express'; @@ -116,16 +116,47 @@ export class QuizzesController { ); } - this.logger.log( - 'info', - `running command "${execCommandDto.message}" for container ${containerId}`, - ); + // 리팩토링 필수입니다. + let message: string, result: string; - const { message, result } = await this.containerService.runGitCommand( - containerId, - execCommandDto.message, - ); + if (execCommandDto.mode === MODE.COMMAND) { + this.logger.log( + 'info', + `running command "${execCommandDto.message}" for container ${containerId}`, + ); + + ({ message, result } = await this.containerService.runGitCommand( + containerId, + execCommandDto.message, + )); + } else if (execCommandDto.mode === MODE.EDITOR) { + const recentCommand = await this.sessionService.getRecentLog( + sessionId, + id, + ); + + const bodyPreview = + execCommandDto.message.length > 15 + ? execCommandDto.message.slice(0, 20) + '...' + : execCommandDto.message; + + this.logger.log( + 'info', + `running editor command "${recentCommand}" for container ${containerId} with body starts with "${bodyPreview}"`, + ); + + ({ message, result } = await this.containerService.runEditorCommand( + containerId, + recentCommand, + execCommandDto.message, + )); + } else { + response.status(HttpStatus.BAD_REQUEST).send({ + message: '잘못된 요청입니다.', + }); + } + // 일단 editor일 때도 message를 저장합니다. this.sessionService.pushLogBySessionId( execCommandDto.message, sessionId, From b7a8b958bd8a6d333659beefb382c5c0843f2b6b Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 10:15:01 +0900 Subject: [PATCH 020/378] =?UTF-8?q?fix:=20CREATED=5FBY=5FOUTPUT.SH=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#72] --- packages/backend/src/containers/containers.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index 0edc347..d447b7b 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -68,7 +68,10 @@ export class ContainersService { ); if (stdoutData.endsWith('# CREATED_BY_OUTPUT.SH\n')) { - return { message: stdoutData, result: 'editor' }; + return { + message: stdoutData.slice(0, -'# CREATED_BY_OUTPUT.SH\n'.length), + result: 'editor', + }; } if (stderrData) { From 4e7ec783ca26cc697c74eea9208c05277a8f0b33 Mon Sep 17 00:00:00 2001 From: YuHyun Date: Tue, 28 Nov 2023 09:56:57 +0900 Subject: [PATCH 021/378] =?UTF-8?q?config:=20msw=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#131] --- packages/frontend/.eslintrc.json | 3 +- packages/frontend/package.json | 3 + packages/frontend/public/mockServiceWorker.js | 303 ++++++++++++++++++ packages/frontend/src/mocks/browser.ts | 6 + packages/frontend/src/mocks/handlers.ts | 3 + packages/frontend/src/mocks/index.ts | 17 + packages/frontend/src/mocks/server.ts | 5 + packages/frontend/src/pages/_app.tsx | 4 + 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/public/mockServiceWorker.js create mode 100644 packages/frontend/src/mocks/browser.ts create mode 100644 packages/frontend/src/mocks/handlers.ts create mode 100644 packages/frontend/src/mocks/index.ts create mode 100644 packages/frontend/src/mocks/server.ts diff --git a/packages/frontend/.eslintrc.json b/packages/frontend/.eslintrc.json index f5197d2..acceeee 100644 --- a/packages/frontend/.eslintrc.json +++ b/packages/frontend/.eslintrc.json @@ -31,7 +31,8 @@ ], "react/require-default-props": "off", "@typescript-eslint/no-use-before-define": "off", - "import/prefer-default-export": "off" + "import/prefer-default-export": "off", + "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] }, "settings": { "import/external-module-folders": [".yarn"] diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 46b2a4d..f1e713b 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -66,5 +66,8 @@ "storybook": "^7.5.3", "typescript": "5.0.0-beta", "webpack": "^5.89.0" + }, + "msw": { + "workerDirectory": "public" } } diff --git a/packages/frontend/public/mockServiceWorker.js b/packages/frontend/public/mockServiceWorker.js new file mode 100644 index 0000000..51d85ee --- /dev/null +++ b/packages/frontend/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.2). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/packages/frontend/src/mocks/browser.ts b/packages/frontend/src/mocks/browser.ts new file mode 100644 index 0000000..1d7af9f --- /dev/null +++ b/packages/frontend/src/mocks/browser.ts @@ -0,0 +1,6 @@ +import { setupWorker } from "msw"; + +import { handlers } from "./handlers"; + +// This configures a Service Worker with the given request handlers. +export const worker = setupWorker(...handlers); diff --git a/packages/frontend/src/mocks/handlers.ts b/packages/frontend/src/mocks/handlers.ts new file mode 100644 index 0000000..9ba4472 --- /dev/null +++ b/packages/frontend/src/mocks/handlers.ts @@ -0,0 +1,3 @@ +import { quizHandlers } from "./apis/quizHandlers"; + +export const handlers = [...quizHandlers]; diff --git a/packages/frontend/src/mocks/index.ts b/packages/frontend/src/mocks/index.ts new file mode 100644 index 0000000..e01d0be --- /dev/null +++ b/packages/frontend/src/mocks/index.ts @@ -0,0 +1,17 @@ +async function initMocks() { + if (typeof window === "undefined") { + const { server } = await import("./server"); + server.listen({ + onUnhandledRequest: "bypass", + }); + } else { + const { worker } = await import("./browser"); + worker.start({ + onUnhandledRequest: "bypass", + }); + } +} + +initMocks(); + +export {}; diff --git a/packages/frontend/src/mocks/server.ts b/packages/frontend/src/mocks/server.ts new file mode 100644 index 0000000..fa95245 --- /dev/null +++ b/packages/frontend/src/mocks/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from "msw/node"; + +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); diff --git a/packages/frontend/src/pages/_app.tsx b/packages/frontend/src/pages/_app.tsx index c7bb1eb..0927aab 100644 --- a/packages/frontend/src/pages/_app.tsx +++ b/packages/frontend/src/pages/_app.tsx @@ -8,6 +8,10 @@ import "react-toastify/dist/ReactToastify.min.css"; import { ToastContainer } from "../design-system/components/common"; import Layout from "../design-system/components/common/Layout"; +if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") { + import("../mocks"); +} + export default function App({ Component, pageProps }: AppProps) { return ( <> From 4665a559469bed4d098cc96807c79886afece067 Mon Sep 17 00:00:00 2001 From: YuHyun Date: Tue, 28 Nov 2023 10:01:36 +0900 Subject: [PATCH 022/378] =?UTF-8?q?feat:=20=EC=A0=95=EB=8B=B5=20=EC=B1=84?= =?UTF-8?q?=EC=A0=90=20API=20=EB=AA=A8=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#131] --- packages/frontend/src/apis/quizzes.ts | 8 ++++++- .../frontend/src/mocks/apis/quizHandlers.ts | 23 +++++++++++++++++++ packages/frontend/src/types/quiz.ts | 11 +++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/mocks/apis/quizHandlers.ts diff --git a/packages/frontend/src/apis/quizzes.ts b/packages/frontend/src/apis/quizzes.ts index 1f579ed..f5b6583 100644 --- a/packages/frontend/src/apis/quizzes.ts +++ b/packages/frontend/src/apis/quizzes.ts @@ -1,5 +1,5 @@ import { API_PATH } from "../constants/path"; -import { Categories, Command, Quiz } from "../types/quiz"; +import { Categories, Command, Quiz, QuizSolve } from "../types/quiz"; import { instance } from "./base"; @@ -19,6 +19,12 @@ export const quizAPI = { const { data } = await instance.get(API_PATH.QUIZZES); return data; }, + submit: async (id: number) => { + const { data } = await instance.post( + `${API_PATH.QUIZZES}/${id}/submit`, + ); + return data; + }, }; type PostCommandRequest = { diff --git a/packages/frontend/src/mocks/apis/quizHandlers.ts b/packages/frontend/src/mocks/apis/quizHandlers.ts new file mode 100644 index 0000000..a5c8534 --- /dev/null +++ b/packages/frontend/src/mocks/apis/quizHandlers.ts @@ -0,0 +1,23 @@ +import { rest } from "msw"; + +import { API_PATH } from "../../constants/path"; +import { isString } from "../../utils/typeGuard"; + +const baseUrl = "/api/v1"; +const quizPath = `${baseUrl}${API_PATH.QUIZZES}`; + +export const quizHandlers = [ + rest.post(`${quizPath}/:id/submit`, (req, res, ctx) => { + const { id } = req.params; + if (!isString(id)) { + return res(ctx.status(404)); + } + + const idNum = parseInt(id, 10); + if (idNum < 1 || idNum > 19) { + return res(ctx.status(404)); + } + + return res(ctx.json({ solved: true, link: "mock-share-link" })); + }), +]; diff --git a/packages/frontend/src/types/quiz.ts b/packages/frontend/src/types/quiz.ts index 3297efb..8c38b8e 100644 --- a/packages/frontend/src/types/quiz.ts +++ b/packages/frontend/src/types/quiz.ts @@ -28,3 +28,14 @@ export type Quizzes = { export type Categories = { categories: Quizzes; }; + +export type QuizSolve = QuizSolveCorrect | QuizSolveWrong; + +export type QuizSolveCorrect = { + solved: true; + link: string; +}; + +export type QuizSolveWrong = { + solved: false; +}; From c22341ae2fc444852b175e16c5f9a902303796b3 Mon Sep 17 00:00:00 2001 From: YuHyun Date: Tue, 28 Nov 2023 11:31:16 +0900 Subject: [PATCH 023/378] =?UTF-8?q?refactor:=20quizzes=20>=20quiz=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#131] --- packages/frontend/src/apis/{quizzes.ts => quiz.ts} | 0 packages/frontend/src/components/demo/Demo.tsx | 2 +- packages/frontend/src/pages/quizzes/[id].tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/frontend/src/apis/{quizzes.ts => quiz.ts} (100%) diff --git a/packages/frontend/src/apis/quizzes.ts b/packages/frontend/src/apis/quiz.ts similarity index 100% rename from packages/frontend/src/apis/quizzes.ts rename to packages/frontend/src/apis/quiz.ts diff --git a/packages/frontend/src/components/demo/Demo.tsx b/packages/frontend/src/components/demo/Demo.tsx index 8d2b729..779ba2b 100644 --- a/packages/frontend/src/components/demo/Demo.tsx +++ b/packages/frontend/src/components/demo/Demo.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { quizAPI } from "../../apis/quizzes"; +import { quizAPI } from "../../apis/quiz"; import { Button } from "../../design-system/components/common"; import { flex } from "../../design-system/tokens/utils.css"; import quizContentMockData from "../../mocks/apis/data/quizContentData"; diff --git a/packages/frontend/src/pages/quizzes/[id].tsx b/packages/frontend/src/pages/quizzes/[id].tsx index 68e345b..d23fb38 100644 --- a/packages/frontend/src/pages/quizzes/[id].tsx +++ b/packages/frontend/src/pages/quizzes/[id].tsx @@ -3,7 +3,7 @@ import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from "next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { quizAPI } from "../../apis/quizzes"; +import { quizAPI } from "../../apis/quiz"; import * as styles from "../../components/demo/Demo.css"; import { CommandAccordion, QuizContent } from "../../components/quiz"; import { Terminal } from "../../components/terminal"; From 7f1d86d1d5747438cd424d8100dce95f18cc12ea Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 18:13:47 +0900 Subject: [PATCH 024/378] =?UTF-8?q?refactor:=20ssh=20util=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#140] Co-author-by: flydog98 --- packages/backend/src/common/ssh.util.ts | 58 +++++++++++++++++ .../src/containers/containers.service.ts | 65 +++---------------- 2 files changed, 67 insertions(+), 56 deletions(-) create mode 100644 packages/backend/src/common/ssh.util.ts diff --git a/packages/backend/src/common/ssh.util.ts b/packages/backend/src/common/ssh.util.ts new file mode 100644 index 0000000..a76343f --- /dev/null +++ b/packages/backend/src/common/ssh.util.ts @@ -0,0 +1,58 @@ +import { Client } from 'ssh2'; +import 'dotenv/config'; + +async function getSSH( + host: string, + port: number, + username: string, + password: string, +): Promise { + const conn = new Client(); + + await new Promise((resolve, reject) => { + conn + .on('ready', () => resolve()) + .on('error', reject) + .connect({ + host, + port, + username, + password, + }); + }); + + return conn; +} +export async function executeSSHCommand( + command: string, +): Promise<{ stdoutData: string; stderrData: string }> { + const conn: Client = await getSSH( + process.env.CONTAINER_SSH_HOST, + Number(process.env.CONTAINER_SSH_PORT), + process.env.CONTAINER_SSH_USERNAME, + process.env.CONTAINER_SSH_PASSWORD, + ); + + return new Promise((resolve, reject) => { + conn.exec(command, (err, stream) => { + if (err) { + reject(new Error('SSH command execution Server error')); + return; + } + let stdoutData = ''; + let stderrData = ''; + stream + .on('close', () => { + conn.end(); + resolve({ stdoutData, stderrData }); + }) + .on('data', (chunk) => { + stdoutData += chunk; + }); + + stream.stderr.on('data', (chunk) => { + stderrData += chunk; + }); + }); + }); +} diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index d447b7b..0547b4c 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Logger } from 'winston'; import { CommandResponseDto } from 'src/quizzes/dto/command-response.dto'; -import { Client } from 'ssh2'; import shellEscape from 'shell-escape'; +import { executeSSHCommand } from '../common/ssh.util'; @Injectable() export class ContainersService { @@ -12,58 +12,11 @@ export class ContainersService { @Inject('winston') private readonly logger: Logger, ) {} - private async getSSH(): Promise { - const conn = new Client(); - - await new Promise((resolve, reject) => { - conn - .on('ready', () => resolve()) - .on('error', reject) - .connect({ - host: this.configService.get('CONTAINER_SSH_HOST'), - port: this.configService.get('CONTAINER_SSH_PORT'), - username: this.configService.get('CONTAINER_SSH_USERNAME'), - password: this.configService.get('CONTAINER_SSH_PASSWORD'), - }); - }); - - return conn; - } - - private async executeSSHCommand( - command: string, - ): Promise<{ stdoutData: string; stderrData: string }> { - const conn: Client = await this.getSSH(); - - return new Promise((resolve, reject) => { - conn.exec(command, (err, stream) => { - if (err) { - reject(new Error('SSH command execution Server error')); - return; - } - let stdoutData = ''; - let stderrData = ''; - stream - .on('close', () => { - conn.end(); - resolve({ stdoutData, stderrData }); - }) - .on('data', (chunk) => { - stdoutData += chunk; - }); - - stream.stderr.on('data', (chunk) => { - stderrData += chunk; - }); - }); - }); - } - async runGitCommand( container: string, command: string, ): Promise { - const { stdoutData, stderrData } = await this.executeSSHCommand( + const { stdoutData, stderrData } = await executeSSHCommand( `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/restricted-shell ${command}`, ); @@ -88,7 +41,7 @@ export class ContainersService { ): Promise { const escapedMessage = shellEscape([message]); - const { stdoutData, stderrData } = await this.executeSSHCommand( + const { stdoutData, stderrData } = await executeSSHCommand( `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} sh -c "git config --global core.editor /editor/input.sh && echo ${escapedMessage} | ${command}; git config --global core.editor /editor/output.sh"`, ); @@ -110,18 +63,18 @@ export class ContainersService { const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ mergemasters/alpine-git:0.2 /bin/sh`; - const { stdoutData } = await this.executeSSHCommand(createContainerCommand); + const { stdoutData } = await executeSSHCommand(createContainerCommand); const containerId = stdoutData.trim(); // TODO: 연속 실행할 때 매번 SSH 하는거 리팩토링 해야 함 const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`; - await this.executeSSHCommand(copyFilesCommand); + await executeSSHCommand(copyFilesCommand); const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}`; - await this.executeSSHCommand(chownCommand); + await executeSSHCommand(chownCommand); const coreEditorCommand = `docker exec -w /home/quizzer/quiz/ -u ${user} ${containerId} git config --global core.editor /editor/output.sh`; - await this.executeSSHCommand(coreEditorCommand); + await executeSSHCommand(coreEditorCommand); return containerId; } @@ -129,7 +82,7 @@ mergemasters/alpine-git:0.2 /bin/sh`; async isValidateContainerId(containerId: string): Promise { const command = `docker ps -a --filter "id=${containerId}" --format "{{.ID}}"`; - const { stdoutData, stderrData } = await this.executeSSHCommand(command); + const { stdoutData, stderrData } = await executeSSHCommand(command); if (stderrData) { // 도커 미설치 등의 에러일 듯 @@ -142,7 +95,7 @@ mergemasters/alpine-git:0.2 /bin/sh`; async deleteContainer(containerId: string): Promise { const command = `docker rm -f ${containerId}`; - const { stdoutData, stderrData } = await this.executeSSHCommand(command); + const { stdoutData, stderrData } = await executeSSHCommand(command); console.log(`container deleted : ${stdoutData}`); From 1ac62da11c8009be025f0aed425a7b0fb3face8f Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 18:17:43 +0900 Subject: [PATCH 025/378] feat: quiz-wizard module add [#140] Co-author-by: flydog98 --- packages/backend/src/app.module.ts | 2 ++ .../src/quiz-wizard/quiz-wizard.module.ts | 8 ++++++++ .../quiz-wizard/quiz-wizard.service.spec.ts | 18 ++++++++++++++++++ packages/backend/src/quizzes/quizzes.module.ts | 2 ++ 4 files changed, 30 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/quiz-wizard.module.ts create mode 100644 packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index d6f0a39..7a2431c 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -10,6 +10,7 @@ import { format } from 'winston'; import { typeOrmConfig } from './configs/typeorm.config'; import { QuizzesModule } from './quizzes/quizzes.module'; import { LoggingInterceptor } from './common/logging.interceptor'; +import { QuizWizardModule } from './quiz-wizard/quiz-wizard.module'; @Module({ imports: [ @@ -38,6 +39,7 @@ import { LoggingInterceptor } from './common/logging.interceptor'; ), ), }), + QuizWizardModule, ], controllers: [AppController], providers: [ diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.module.ts b/packages/backend/src/quiz-wizard/quiz-wizard.module.ts new file mode 100644 index 0000000..52e7b41 --- /dev/null +++ b/packages/backend/src/quiz-wizard/quiz-wizard.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { QuizWizardService } from './quiz-wizard.service'; + +@Module({ + providers: [QuizWizardService], + exports: [QuizWizardService], +}) +export class QuizWizardModule {} diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts new file mode 100644 index 0000000..a4e5296 --- /dev/null +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizWizardService } from './quiz-wizard.service'; + +describe('QuizWizardService', () => { + let service: QuizWizardService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [QuizWizardService], + }).compile(); + + service = module.get(QuizWizardService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/backend/src/quizzes/quizzes.module.ts b/packages/backend/src/quizzes/quizzes.module.ts index 6672409..139208b 100644 --- a/packages/backend/src/quizzes/quizzes.module.ts +++ b/packages/backend/src/quizzes/quizzes.module.ts @@ -7,12 +7,14 @@ import { Category } from './entity/category.entity'; import { ContainersModule } from '../containers/containers.module'; import { SessionModule } from '../session/session.module'; import { Keyword } from './entity/keyword.entity'; +import { QuizWizardModule } from '../quiz-wizard/quiz-wizard.module'; @Module({ imports: [ TypeOrmModule.forFeature([Quiz, Category, Keyword]), ContainersModule, SessionModule, + QuizWizardModule, ], controllers: [QuizzesController], providers: [QuizzesService], From 2d82aa3cb2587dff35fc4c711cb353927fb58593 Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 18:20:09 +0900 Subject: [PATCH 026/378] feat: test.module add, submit controller edit [#140] --- packages/backend/package.json | 5 +- .../src/quiz-wizard/quiz-wizard.service.ts | 36 +++++++++++++ .../src/quiz-wizard/tests/test.module.ts | 23 ++++++++ .../backend/src/quizzes/dto/submit.dto.ts | 23 ++++++++ .../backend/src/quizzes/quizzes.controller.ts | 53 +++++++++++++++++++ yarn.lock | 7 +-- 6 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/quiz-wizard/quiz-wizard.service.ts create mode 100644 packages/backend/src/quiz-wizard/tests/test.module.ts create mode 100644 packages/backend/src/quizzes/dto/submit.dto.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index b0900ee..9dffae1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "dotenv": "^16.3.1", "mongoose": "^8.0.1", "nest-winston": "^1.9.4", "papaparse": "^5.4.1", @@ -46,7 +47,8 @@ "ssh2": "^1.14.0", "typeorm": "^0.3.17", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1" + "winston-daily-rotate-file": "^4.7.1", + "jest": "^29.7.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -65,7 +67,6 @@ "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", "lint-staged": "^15.1.0", "prettier": "^3.1.0", "source-map-support": "^0.5.21", diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts new file mode 100644 index 0000000..1efc6e2 --- /dev/null +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import { exec } from 'child_process'; + +@Injectable() +export class QuizWizardService { + async submit(containerId, quizId) { + const execAsync = promisify(exec); + + console.log('현재 디렉토리 위치:', process.cwd()); + try { + await execAsync( + `yarn run jest ${path.resolve( + process.cwd(), + 'src', + 'quiz-wizard', + 'tests', + `${quizId}.spec.ts`, + )} ${containerId}`, + ); + return true; + } catch (e) { + return false; + } + } + checkDirectoryExists(directoryPath) { + const doesExist = fs.existsSync(directoryPath); + describe('Directory Existence Check', () => { + it('should check if a directory exists', () => { + expect(doesExist).toBe(true); + }); + }); + } +} diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/tests/test.module.ts new file mode 100644 index 0000000..e681597 --- /dev/null +++ b/packages/backend/src/quiz-wizard/tests/test.module.ts @@ -0,0 +1,23 @@ +import { executeSSHCommand } from '../../common/ssh.util'; + +export async function isDirectoryExist( + container: string, + path: string, +): Promise { + const { stdoutData } = await executeSSHCommand( + `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/sh "ls -l ${path} | grep ^d"`, + ); + + return stdoutData !== ''; +} + +export async function getConfig( + container: string, + key: string, +): Promise { + const { stdoutData } = await executeSSHCommand( + `docker exec -u quizzer -w /home/quizzer/quiz ${container} git -C /home/quizzer/quiz config user.${key}`, + ); + + return stdoutData; +} diff --git a/packages/backend/src/quizzes/dto/submit.dto.ts b/packages/backend/src/quizzes/dto/submit.dto.ts new file mode 100644 index 0000000..42e3c06 --- /dev/null +++ b/packages/backend/src/quizzes/dto/submit.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsString } from 'class-validator'; + +export class Success { + @ApiProperty({ example: true }) + @IsBoolean() + solved = true; + + @ApiProperty({ example: 'gitchallenge.com/api/v1/quizzes/shared?answer=””' }) + @IsString() + link: string; + + constructor(link: string) { + this.link = link; + } +} + +export class Fail { + @ApiProperty({ example: false }) + solved = false; +} + +export type SubmitDto = Success | Fail; diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index 483b698..77e2744 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -30,6 +30,8 @@ import { ContainersService } from '../containers/containers.service'; import { SessionId } from '../session/session.decorator'; import { SessionGuard } from '../session/session.guard'; import { CommandGuard } from '../command.guard'; +import { QuizWizardService } from '../quiz-wizard/quiz-wizard.service'; +import { Fail, SubmitDto, Success } from './dto/submit.dto'; @ApiTags('quizzes') @Controller('api/v1/quizzes') @@ -38,6 +40,7 @@ export class QuizzesController { private readonly quizService: QuizzesService, private readonly sessionService: SessionService, private readonly containerService: ContainersService, + private readonly quizWizardService: QuizWizardService, @Inject('winston') private readonly logger: Logger, ) {} @@ -216,4 +219,54 @@ export class QuizzesController { ); } } + + @Post(':id/submit') + @UseGuards(SessionGuard) + @ApiOperation({ summary: '채점을 요청합니다.' }) + @ApiResponse({ + status: 200, + description: '채점 결과를 리턴합니다.', + type: Success, + }) + @ApiParam({ name: 'id', description: '문제 ID' }) + async submit( + @Param('id') id: number, + @SessionId() sessionId: string, + ): Promise { + try { + const containerId = await this.sessionService.getContainerIdBySessionId( + sessionId, + id, + ); + + if (!containerId) { + return; + } + + if (!(await this.containerService.isValidateContainerId(containerId))) { + // 재현해서 컨테이너 발급하기 + return; + } + + const result: boolean = await this.quizWizardService.submit( + containerId, + id, + ); + + if (!result) { + return new Fail(); + } + + //create link + return new Success('notImplemented'); + } catch (e) { + throw new HttpException( + { + message: 'Internal Server Error', + result: 'fail', + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/yarn.lock b/yarn.lock index 291337a..aaf2b75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6887,10 +6887,11 @@ __metadata: class-transformer: "npm:^0.5.1" class-validator: "npm:^0.14.0" cookie-parser: "npm:^1.4.6" + dotenv: "npm:^16.3.1" eslint: "npm:^8.53.0" eslint-config-prettier: "npm:^9.0.0" eslint-plugin-prettier: "npm:^5.0.0" - jest: "npm:^29.5.0" + jest: "npm:^29.7.0" lint-staged: "npm:^15.1.0" mongoose: "npm:^8.0.1" nest-winston: "npm:^1.9.4" @@ -8747,7 +8748,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:16.3.1, dotenv@npm:^16.0.0, dotenv@npm:^16.0.3": +"dotenv@npm:16.3.1, dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.1": version: 16.3.1 resolution: "dotenv@npm:16.3.1" checksum: b95ff1bbe624ead85a3cd70dbd827e8e06d5f05f716f2d0cbc476532d54c7c9469c3bc4dd93ea519f6ad711cb522c00ac9a62b6eb340d5affae8008facc3fbd7 @@ -12394,7 +12395,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.5.0, jest@npm:^29.7.0": +"jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: From a8c5bcc30ce8e28c7b4aeadf865ea391ed01751f Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 18:25:06 +0900 Subject: [PATCH 027/378] feat: 1.spec.ts add [#141] --- packages/backend/src/quiz-wizard/tests/1.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/tests/1.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/1.spec.ts b/packages/backend/src/quiz-wizard/tests/1.spec.ts new file mode 100644 index 0000000..5c3057b --- /dev/null +++ b/packages/backend/src/quiz-wizard/tests/1.spec.ts @@ -0,0 +1,9 @@ +import { isDirectoryExist } from './test.module'; + +const containerId = process.argv[3]; + +describe('QuizWizardService', () => { + it('should be true', async () => { + expect(await isDirectoryExist(containerId, '.git')).toBe(true); + }); +}); From 0e343532efeb71dcab5d7a74c49d72652f385282 Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 18:27:52 +0900 Subject: [PATCH 028/378] feat: 2.spec.ts add [#143] --- packages/backend/src/quiz-wizard/tests/2.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/tests/2.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/2.spec.ts b/packages/backend/src/quiz-wizard/tests/2.spec.ts new file mode 100644 index 0000000..8e140a3 --- /dev/null +++ b/packages/backend/src/quiz-wizard/tests/2.spec.ts @@ -0,0 +1,12 @@ +import { getConfig } from './test.module'; + +const containerId = process.argv[3]; + +describe('QuizWizardService', () => { + it('should be true', async () => { + expect(await getConfig(containerId, 'name')).not.toMatch('MergeMaster'); + expect(await getConfig(containerId, 'email')).not.toMatch( + 'mergemaster@gitchallenge.com', + ); + }); +}); From 5e92fce1b4b6ef54ef8a5f36abc78e301ecde3fc Mon Sep 17 00:00:00 2001 From: luizy Date: Tue, 28 Nov 2023 19:03:00 +0900 Subject: [PATCH 029/378] feat: command guard edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [';', '>', '|', '<'] block - 디렉토리 common으로 이동 [#167] --- packages/backend/src/{ => common}/command.guard.ts | 8 +++++++- packages/backend/src/quizzes/quizzes.controller.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) rename packages/backend/src/{ => common}/command.guard.ts (58%) diff --git a/packages/backend/src/command.guard.ts b/packages/backend/src/common/command.guard.ts similarity index 58% rename from packages/backend/src/command.guard.ts rename to packages/backend/src/common/command.guard.ts index c6a6c1a..da264a4 100644 --- a/packages/backend/src/command.guard.ts +++ b/packages/backend/src/common/command.guard.ts @@ -9,7 +9,13 @@ export class CommandGuard implements CanActivate { return ( typeof mode === 'string' && typeof message === 'string' && - (mode === 'editor' || (mode === 'command' && message.startsWith('git'))) + (mode === 'editor' || + (mode === 'command' && + message.startsWith('git') && + !this.isMessageIncluded(message, [';', '>', '|', '<']))) ); } + private isMessageIncluded(message: string, keywords: string[]): boolean { + return keywords.some((keyword) => message.includes(keyword)); + } } diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index 77e2744..e425b6b 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -29,7 +29,7 @@ import { Response } from 'express'; import { ContainersService } from '../containers/containers.service'; import { SessionId } from '../session/session.decorator'; import { SessionGuard } from '../session/session.guard'; -import { CommandGuard } from '../command.guard'; +import { CommandGuard } from '../common/command.guard'; import { QuizWizardService } from '../quiz-wizard/quiz-wizard.service'; import { Fail, SubmitDto, Success } from './dto/submit.dto'; From cbbf7bc3c7c18bb7aabc870757bbdb40de6fb2c2 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:52:57 +0900 Subject: [PATCH 030/378] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20console.log=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../backend/src/quiz-wizard/quiz-wizard.service.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts index 1efc6e2..a863b32 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { exec } from 'child_process'; @@ -9,7 +8,6 @@ export class QuizWizardService { async submit(containerId, quizId) { const execAsync = promisify(exec); - console.log('현재 디렉토리 위치:', process.cwd()); try { await execAsync( `yarn run jest ${path.resolve( @@ -25,12 +23,4 @@ export class QuizWizardService { return false; } } - checkDirectoryExists(directoryPath) { - const doesExist = fs.existsSync(directoryPath); - describe('Directory Existence Check', () => { - it('should check if a directory exists', () => { - expect(doesExist).toBe(true); - }); - }); - } } From d301eeff30735133bf00420b7323c4fbce6869b9 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:53:19 +0900 Subject: [PATCH 031/378] =?UTF-8?q?fix:=20=EB=B6=80=EC=A0=95=ED=99=95?= =?UTF-8?q?=ED=95=9C=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 분명 같이 작업해서 괜찮은 줄 알았어요... [#145] --- packages/backend/src/quiz-wizard/tests/test.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/tests/test.module.ts index e681597..1faadbe 100644 --- a/packages/backend/src/quiz-wizard/tests/test.module.ts +++ b/packages/backend/src/quiz-wizard/tests/test.module.ts @@ -5,7 +5,7 @@ export async function isDirectoryExist( path: string, ): Promise { const { stdoutData } = await executeSSHCommand( - `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/sh "ls -l ${path} | grep ^d"`, + `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} ls -l ${path} | grep ^d`, ); return stdoutData !== ''; From cd165b29f77b93bb549d2d0c147d9cc90310fe2c Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:53:54 +0900 Subject: [PATCH 032/378] =?UTF-8?q?test:=20containerService=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=AA=A8=ED=82=B9=20=EB=B0=8F=201?= =?UTF-8?q?=EB=B2=88=20=EB=AC=B8=EC=A0=9C=20=EC=B1=84=EC=A0=90=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index a4e5296..50453ce 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -1,18 +1,64 @@ import { Test, TestingModule } from '@nestjs/testing'; import { QuizWizardService } from './quiz-wizard.service'; +import { ContainersService } from '../containers/containers.service'; +import { executeSSHCommand } from '../common/ssh.util'; +import { ConfigService } from '@nestjs/config'; + +const mockConfigService = { + get: jest.fn((key) => { + if (key === 'CONTAINER_GIT_USERNAME') { + return 'quizzer'; + } + }), +}; +const mockLogger = {}; describe('QuizWizardService', () => { let service: QuizWizardService; + let containerService: ContainersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [QuizWizardService], + providers: [ + QuizWizardService, + ContainersService, + ContainersService, + { + provide: ConfigService, + useValue: mockConfigService, // 여기서 mockConfigService는 모킹된 ConfigService + }, + { + provide: 'winston', + useValue: mockLogger, // 여기서 mockLogger는 모킹된 Logger + }, + ], }).compile(); service = module.get(QuizWizardService); + containerService = module.get(ContainersService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('1번 문제 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(1); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('init 이후 정답 받는 경우', async () => { + await executeSSHCommand( + `docker exec -w /home/quizzer/quiz -u quizzer ${containerId} git init`, + ); + + expect(await service.submit(containerId, 1)).toBe(true); + }); + + it('init 하지 않은 경우', async () => { + expect(await service.submit(containerId, 1)).toBe(false); + }); }); }); From 96c5b1ff8965abbbd675daff54bb083614b1fd9e Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:26:08 +0900 Subject: [PATCH 033/378] =?UTF-8?q?config:=20uuid=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/package.json | 6 ++++-- yarn.lock | 11 ++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 9dffae1..72c6456 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", + "jest": "^29.7.0", "mongoose": "^8.0.1", "nest-winston": "^1.9.4", "papaparse": "^5.4.1", @@ -46,9 +47,9 @@ "sqlite3": "^5.1.6", "ssh2": "^1.14.0", "typeorm": "^0.3.17", + "uuid": "^9.0.1", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1", - "jest": "^29.7.0" + "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -62,6 +63,7 @@ "@types/shell-escape": "^0.2.3", "@types/ssh2": "^1", "@types/supertest": "^2.0.12", + "@types/uuid": "^9", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.53.0", diff --git a/yarn.lock b/yarn.lock index aaf2b75..b5f5742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5507,6 +5507,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9": + version: 9.0.7 + resolution: "@types/uuid@npm:9.0.7" + checksum: b329ebd4f9d1d8e08d4f2cc211be4922d70d1149f73d5772630e4a3acfb5170c6d37b3d7a39a0412f1a56e86e8a844c7f297c798b082f90380608bf766688787 + languageName: node + linkType: hard + "@types/validator@npm:^13.7.10": version: 13.11.7 resolution: "@types/validator@npm:13.11.7" @@ -6882,6 +6889,7 @@ __metadata: "@types/shell-escape": "npm:^0.2.3" "@types/ssh2": "npm:^1" "@types/supertest": "npm:^2.0.12" + "@types/uuid": "npm:^9" "@typescript-eslint/eslint-plugin": "npm:^6.0.0" "@typescript-eslint/parser": "npm:^6.0.0" class-transformer: "npm:^0.5.1" @@ -6910,6 +6918,7 @@ __metadata: tsconfig-paths: "npm:^4.2.0" typeorm: "npm:^0.3.17" typescript: "npm:^5.0.0-beta" + uuid: "npm:^9.0.1" winston: "npm:^3.11.0" winston-daily-rotate-file: "npm:^4.7.1" languageName: unknown @@ -17971,7 +17980,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.0": +"uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: From a740ddf535fa84f02d73da5f66742f079fed50c8 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:38:47 +0900 Subject: [PATCH 034/378] =?UTF-8?q?perf:=20SSH=20=EC=97=B0=EC=86=8D=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20SSH?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=ED=95=9C=20=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=EC=88=98=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 코드 작성 중 시간이 오래 걸리는 거 같아서 지금 태스크가 아니지만 개선해 봤습니다... - 약 2초 정도의 시간을 세이브합니다. [#145] --- packages/backend/src/common/ssh.util.ts | 58 ++++++++++++------- .../src/containers/containers.service.ts | 20 +++---- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/common/ssh.util.ts b/packages/backend/src/common/ssh.util.ts index a76343f..0c554c0 100644 --- a/packages/backend/src/common/ssh.util.ts +++ b/packages/backend/src/common/ssh.util.ts @@ -24,7 +24,7 @@ async function getSSH( return conn; } export async function executeSSHCommand( - command: string, + ...commands: string[] ): Promise<{ stdoutData: string; stderrData: string }> { const conn: Client = await getSSH( process.env.CONTAINER_SSH_HOST, @@ -33,26 +33,42 @@ export async function executeSSHCommand( process.env.CONTAINER_SSH_PASSWORD, ); - return new Promise((resolve, reject) => { - conn.exec(command, (err, stream) => { - if (err) { - reject(new Error('SSH command execution Server error')); - return; - } - let stdoutData = ''; - let stderrData = ''; - stream - .on('close', () => { - conn.end(); - resolve({ stdoutData, stderrData }); - }) - .on('data', (chunk) => { - stdoutData += chunk; - }); + const result = await commands.reduce( + async (prevPromise, command) => { + const accum = await prevPromise; + const { stdoutData, stderrData } = await new Promise<{ + stdoutData: string; + stderrData: string; + }>((resolve, reject) => { + conn.exec(command, (err, stream) => { + if (err) { + reject(new Error('SSH command execution Server error')); + return; + } + let stdoutData = ''; + let stderrData = ''; + stream + .on('close', () => { + resolve({ stdoutData, stderrData }); + }) + .on('data', (chunk) => { + stdoutData += chunk; + }); - stream.stderr.on('data', (chunk) => { - stderrData += chunk; + stream.stderr.on('data', (chunk) => { + stderrData += chunk; + }); + }); }); - }); - }); + + return { + stdoutData: accum.stdoutData + stdoutData, + stderrData: accum.stderrData + stderrData, + }; + }, + Promise.resolve({ stdoutData: '', stderrData: '' }), + ); + + conn.end(); + return result; } diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index 0547b4c..f55a0e8 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -4,6 +4,7 @@ import { Logger } from 'winston'; import { CommandResponseDto } from 'src/quizzes/dto/command-response.dto'; import shellEscape from 'shell-escape'; import { executeSSHCommand } from '../common/ssh.util'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class ContainersService { @@ -61,20 +62,19 @@ export class ContainersService { 'CONTAINER_GIT_USERNAME', ); - const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ -mergemasters/alpine-git:0.2 /bin/sh`; - const { stdoutData } = await executeSSHCommand(createContainerCommand); - const containerId = stdoutData.trim(); + const containerId = uuidv4(); - // TODO: 연속 실행할 때 매번 SSH 하는거 리팩토링 해야 함 + const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ +--name ${containerId} mergemasters/alpine-git:0.2 /bin/sh`; const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`; - await executeSSHCommand(copyFilesCommand); - const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}`; - await executeSSHCommand(chownCommand); - const coreEditorCommand = `docker exec -w /home/quizzer/quiz/ -u ${user} ${containerId} git config --global core.editor /editor/output.sh`; - await executeSSHCommand(coreEditorCommand); + await executeSSHCommand( + createContainerCommand, + copyFilesCommand, + chownCommand, + coreEditorCommand, + ); return containerId; } From a04bc38e6a3673ad3b7c49a0316cae847870340e Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:41:41 +0900 Subject: [PATCH 035/378] =?UTF-8?q?test:=202=EB=B2=88=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EC=B1=84=EC=A0=90=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index 50453ce..562fb11 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -4,6 +4,8 @@ import { ContainersService } from '../containers/containers.service'; import { executeSSHCommand } from '../common/ssh.util'; import { ConfigService } from '@nestjs/config'; +const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; + const mockConfigService = { get: jest.fn((key) => { if (key === 'CONTAINER_GIT_USERNAME') { @@ -38,7 +40,7 @@ describe('QuizWizardService', () => { containerService = module.get(ContainersService); }); - describe('1번 문제 테스트 코드', () => { + describe('1번 문제 채점 테스트 코드', () => { let containerId: string; beforeEach(async () => { @@ -50,9 +52,7 @@ describe('QuizWizardService', () => { }); it('init 이후 정답 받는 경우', async () => { - await executeSSHCommand( - `docker exec -w /home/quizzer/quiz -u quizzer ${containerId} git init`, - ); + await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); expect(await service.submit(containerId, 1)).toBe(true); }); @@ -61,4 +61,29 @@ describe('QuizWizardService', () => { expect(await service.submit(containerId, 1)).toBe(false); }); }); + + describe('2번 문제 채점 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(2); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, + `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, + ); + + expect(await service.submit(containerId, 2)).toBe(true); + }); + + it('아무 동작 없는 케이스', async () => { + expect(await service.submit(containerId, 2)).toBe(false); + }); + }); }); From ff2369015c69ab6c3f1f661cf371e073aad521a8 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:25:55 +0900 Subject: [PATCH 036/378] =?UTF-8?q?fix:=20id=EA=B0=80=20=EC=95=84=EB=8B=88?= =?UTF-8?q?=EB=9D=BC=20name=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=90=98=EC=96=B4=20=EB=B0=9C=EC=83=9D=ED=95=9C=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/src/containers/containers.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index f55a0e8..a04caf1 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -80,7 +80,7 @@ export class ContainersService { } async isValidateContainerId(containerId: string): Promise { - const command = `docker ps -a --filter "id=${containerId}" --format "{{.ID}}"`; + const command = `docker ps -a --filter "name=${containerId}" --format "{{.ID}}"`; const { stdoutData, stderrData } = await executeSSHCommand(command); From e9eefaaaa78e89a8b4d780473f61cbc908867cf7 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:34:13 +0900 Subject: [PATCH 037/378] =?UTF-8?q?feat:=203=EB=B2=88=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EC=B1=84=EC=A0=90=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 28 +++++++++++++++++++ .../backend/src/quiz-wizard/tests/3.spec.ts | 19 +++++++++++++ .../src/quiz-wizard/tests/test.module.ts | 8 ++++++ 3 files changed, 55 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/tests/3.spec.ts diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index 562fb11..1af641d 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -86,4 +86,32 @@ describe('QuizWizardService', () => { expect(await service.submit(containerId, 2)).toBe(false); }); }); + + describe('3번 문제 채점 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(3); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(true); + }); + + it('전부 다 add한 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(false); + }); + }); }); diff --git a/packages/backend/src/quiz-wizard/tests/3.spec.ts b/packages/backend/src/quiz-wizard/tests/3.spec.ts new file mode 100644 index 0000000..c994274 --- /dev/null +++ b/packages/backend/src/quiz-wizard/tests/3.spec.ts @@ -0,0 +1,19 @@ +import { readGitIndex } from './test.module'; + +const containerId = process.argv[3]; + +describe('QuizWizardService', () => { + it('should be true', async () => { + expect(await readGitIndex(containerId)) + .toMatch(`diff --git a/README.md b/README.md +new file mode 100644 +index 0000000..e69de29 +diff --git a/docs/plan.md b/docs/plan.md +index e69de29..3b18e51 100644 +--- a/docs/plan.md ++++ b/docs/plan.md +@@ -0,0 +1 @@ ++hello world +`); + }); +}); diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/tests/test.module.ts index 1faadbe..e558a42 100644 --- a/packages/backend/src/quiz-wizard/tests/test.module.ts +++ b/packages/backend/src/quiz-wizard/tests/test.module.ts @@ -21,3 +21,11 @@ export async function getConfig( return stdoutData; } + +export async function readGitIndex(container: string): Promise { + const { stdoutData } = await executeSSHCommand( + `docker exec -u quizzer -w /home/quizzer/quiz ${container} git diff --cached`, + ); + + return stdoutData; +} From 7e8d91c50c1782acb8eabdaa4af8f35729020d85 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:37:45 +0900 Subject: [PATCH 038/378] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../backend/src/quiz-wizard/{tests => comparers}/1.spec.ts | 0 .../backend/src/quiz-wizard/{tests => comparers}/2.spec.ts | 0 .../backend/src/quiz-wizard/{tests => comparers}/3.spec.ts | 0 .../src/quiz-wizard/{tests => comparers}/test.module.ts | 0 packages/backend/src/quiz-wizard/quiz-wizard.service.ts | 2 +- .../src/quiz-wizard/{ => test}/quiz-wizard.service.spec.ts | 6 +++--- 6 files changed, 4 insertions(+), 4 deletions(-) rename packages/backend/src/quiz-wizard/{tests => comparers}/1.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/2.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/3.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/test.module.ts (100%) rename packages/backend/src/quiz-wizard/{ => test}/quiz-wizard.service.spec.ts (94%) diff --git a/packages/backend/src/quiz-wizard/tests/1.spec.ts b/packages/backend/src/quiz-wizard/comparers/1.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/1.spec.ts rename to packages/backend/src/quiz-wizard/comparers/1.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/2.spec.ts b/packages/backend/src/quiz-wizard/comparers/2.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/2.spec.ts rename to packages/backend/src/quiz-wizard/comparers/2.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/3.spec.ts b/packages/backend/src/quiz-wizard/comparers/3.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/3.spec.ts rename to packages/backend/src/quiz-wizard/comparers/3.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/test.module.ts rename to packages/backend/src/quiz-wizard/comparers/test.module.ts diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts index a863b32..0dc9da5 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts @@ -14,7 +14,7 @@ export class QuizWizardService { process.cwd(), 'src', 'quiz-wizard', - 'tests', + 'comparers', `${quizId}.spec.ts`, )} ${containerId}`, ); diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts similarity index 94% rename from packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts rename to packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts index 1af641d..1551e07 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { QuizWizardService } from './quiz-wizard.service'; -import { ContainersService } from '../containers/containers.service'; -import { executeSSHCommand } from '../common/ssh.util'; +import { QuizWizardService } from '../quiz-wizard.service'; +import { ContainersService } from '../../containers/containers.service'; +import { executeSSHCommand } from '../../common/ssh.util'; import { ConfigService } from '@nestjs/config'; const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; From a32a9342da15efcb02c1d1b6ef421863c94086c5 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:05:01 +0900 Subject: [PATCH 039/378] =?UTF-8?q?config:=20jest=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컨테이너 생성하고 SSH 하는 로직이 기본 timeout인 5초보다 긴 경우가 많아서 10초로 했습니다. [#145] --- packages/backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 72c6456..fe63f3e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -94,6 +94,7 @@ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "testTimeout": 10000 } } From 7ecae11fc0e5f73d97911075c255d96746888ac2 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:05:36 +0900 Subject: [PATCH 040/378] =?UTF-8?q?perf:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=B5=ED=86=B5=20=EC=84=A4=EC=A0=95=20export?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=91?= =?UTF-8?q?=EB=A0=AC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 코드는 테스트가 순차적으로 실행되어서 50초까지도 걸렸음 - 문제 3개 기준 20초 정도로 빠르게 실행 가능 [#145] --- .../src/quiz-wizard/test/comparer1.spec.ts | 25 ++++ .../src/quiz-wizard/test/comparer2.spec.ts | 28 +++++ .../src/quiz-wizard/test/comparer3.spec.ts | 31 +++++ .../test/quiz-wizard.service.spec.ts | 117 ------------------ .../test/quiz-wizard.test.setup.ts | 39 ++++++ 5 files changed, 123 insertions(+), 117 deletions(-) create mode 100644 packages/backend/src/quiz-wizard/test/comparer1.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/comparer2.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/comparer3.spec.ts delete mode 100644 packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts diff --git a/packages/backend/src/quiz-wizard/test/comparer1.spec.ts b/packages/backend/src/quiz-wizard/test/comparer1.spec.ts new file mode 100644 index 0000000..50bce33 --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer1.spec.ts @@ -0,0 +1,25 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('1번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(1)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('init 이후 정답 받는 경우', async () => { + await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); + + expect(await service.submit(containerId, 1)).toBe(true); + }); + + it('init 하지 않은 경우', async () => { + expect(await service.submit(containerId, 1)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/comparer2.spec.ts b/packages/backend/src/quiz-wizard/test/comparer2.spec.ts new file mode 100644 index 0000000..b7158ba --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer2.spec.ts @@ -0,0 +1,28 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('2번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(2)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, + `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, + ); + + expect(await service.submit(containerId, 2)).toBe(true); + }); + + it('아무 동작 없는 케이스', async () => { + expect(await service.submit(containerId, 2)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/comparer3.spec.ts b/packages/backend/src/quiz-wizard/test/comparer3.spec.ts new file mode 100644 index 0000000..d195186 --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer3.spec.ts @@ -0,0 +1,31 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('3번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(3)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(true); + }); + + it('전부 다 add한 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts deleted file mode 100644 index 1551e07..0000000 --- a/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { QuizWizardService } from '../quiz-wizard.service'; -import { ContainersService } from '../../containers/containers.service'; -import { executeSSHCommand } from '../../common/ssh.util'; -import { ConfigService } from '@nestjs/config'; - -const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; - -const mockConfigService = { - get: jest.fn((key) => { - if (key === 'CONTAINER_GIT_USERNAME') { - return 'quizzer'; - } - }), -}; -const mockLogger = {}; - -describe('QuizWizardService', () => { - let service: QuizWizardService; - let containerService: ContainersService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - QuizWizardService, - ContainersService, - ContainersService, - { - provide: ConfigService, - useValue: mockConfigService, // 여기서 mockConfigService는 모킹된 ConfigService - }, - { - provide: 'winston', - useValue: mockLogger, // 여기서 mockLogger는 모킹된 Logger - }, - ], - }).compile(); - - service = module.get(QuizWizardService); - containerService = module.get(ContainersService); - }); - - describe('1번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(1); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('init 이후 정답 받는 경우', async () => { - await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); - - expect(await service.submit(containerId, 1)).toBe(true); - }); - - it('init 하지 않은 경우', async () => { - expect(await service.submit(containerId, 1)).toBe(false); - }); - }); - - describe('2번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(2); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('베스트 정답 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, - `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, - ); - - expect(await service.submit(containerId, 2)).toBe(true); - }); - - it('아무 동작 없는 케이스', async () => { - expect(await service.submit(containerId, 2)).toBe(false); - }); - }); - - describe('3번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(3); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('베스트 정답 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, - ); - - expect(await service.submit(containerId, 3)).toBe(true); - }); - - it('전부 다 add한 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, - ); - - expect(await service.submit(containerId, 3)).toBe(false); - }); - }); -}); diff --git a/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts new file mode 100644 index 0000000..1b637ff --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts @@ -0,0 +1,39 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizWizardService } from '../quiz-wizard.service'; +import { ContainersService } from '../../containers/containers.service'; +import { ConfigService } from '@nestjs/config'; + +export const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; + +const mockConfigService = { + get: jest.fn((key) => { + if (key === 'CONTAINER_GIT_USERNAME') { + return 'quizzer'; + } + }), +}; +const mockLogger = {}; + +export const setupTestingModule = async (quizId: number) => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizWizardService, + ContainersService, + ContainersService, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: 'winston', + useValue: mockLogger, + }, + ], + }).compile(); + + const service = module.get(QuizWizardService); + const containerService = module.get(ContainersService); + const containerId = await containerService.getContainer(quizId); + + return { service, containerService, containerId }; +}; From 34353746183770e75e017c2fc0381ceed7a053db Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:37:06 +0900 Subject: [PATCH 041/378] =?UTF-8?q?refactor:=20=EC=A1=B0=EA=B8=88=20?= =?UTF-8?q?=EB=8D=94=20=EC=A0=95=ED=99=95=ED=95=9C=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/src/quiz-wizard/comparers/3.spec.ts | 4 ++-- packages/backend/src/quiz-wizard/comparers/test.module.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/quiz-wizard/comparers/3.spec.ts b/packages/backend/src/quiz-wizard/comparers/3.spec.ts index c994274..519bcf8 100644 --- a/packages/backend/src/quiz-wizard/comparers/3.spec.ts +++ b/packages/backend/src/quiz-wizard/comparers/3.spec.ts @@ -1,10 +1,10 @@ -import { readGitIndex } from './test.module'; +import { getCachedDiff } from './test.module'; const containerId = process.argv[3]; describe('QuizWizardService', () => { it('should be true', async () => { - expect(await readGitIndex(containerId)) + expect(await getCachedDiff(containerId)) .toMatch(`diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/quiz-wizard/comparers/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts index e558a42..1f16771 100644 --- a/packages/backend/src/quiz-wizard/comparers/test.module.ts +++ b/packages/backend/src/quiz-wizard/comparers/test.module.ts @@ -22,7 +22,7 @@ export async function getConfig( return stdoutData; } -export async function readGitIndex(container: string): Promise { +export async function getCachedDiff(container: string): Promise { const { stdoutData } = await executeSSHCommand( `docker exec -u quizzer -w /home/quizzer/quiz ${container} git diff --cached`, ); From 727702df466eedb26874c09e26993483f059d64e Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 03:07:17 +0900 Subject: [PATCH 042/378] =?UTF-8?q?fix:=20uuid=20name=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20containerID=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20default=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#147] --- packages/backend/src/session/session.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index 06ab141..efd1346 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -37,7 +37,7 @@ export class SessionService { session.problems.set(problemId, { status: 'solving', logs: [], - containerId: '', + containerId: 'default-id', }); this.logger.log('info', `session ${session._id as ObjectId} updated`); this.logger.log( From 720f5db14d85fcc03398d00843083289f2497daa Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 04:00:08 +0900 Subject: [PATCH 043/378] =?UTF-8?q?config:=20=EC=B1=84=EC=A0=90=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=B4=20=EC=A1=B0=EA=B8=88=20=EC=B4=89?= =?UTF-8?q?=EB=B0=95=ED=95=B4=EC=84=9C=2020=EC=B4=88=EB=A1=9C=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#147] --- packages/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index fe63f3e..d47ed5c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -95,6 +95,6 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node", - "testTimeout": 10000 + "testTimeout": 20000 } } From 3de8d8ea3c0379b19d9c1c43facad4a742959767 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 04:00:31 +0900 Subject: [PATCH 044/378] =?UTF-8?q?feat:=204=EB=B2=88=20=EB=AC=B8=EC=B2=B4?= =?UTF-8?q?=20=EC=B1=84=EC=A0=90=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getTreeHead 모듈 또한 작성했습니다. 특정 브랜치의 최신 커밋의 트리 해시를 얻습니다. [#147] --- .../src/quiz-wizard/comparers/4.spec.ts | 11 +++++++ .../src/quiz-wizard/comparers/test.module.ts | 11 +++++++ .../src/quiz-wizard/test/comparer4.spec.ts | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/comparers/4.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/comparer4.spec.ts diff --git a/packages/backend/src/quiz-wizard/comparers/4.spec.ts b/packages/backend/src/quiz-wizard/comparers/4.spec.ts new file mode 100644 index 0000000..abbaabe --- /dev/null +++ b/packages/backend/src/quiz-wizard/comparers/4.spec.ts @@ -0,0 +1,11 @@ +import { getTreeHead } from './test.module'; + +const containerId = process.argv[3]; + +describe('QuizWizardService', () => { + it('should be true', async () => { + expect(await getTreeHead(containerId, 'main')).toMatch( + '2c347f65f96ed5817553d668c062f8bec792131d', + ); + }); +}); diff --git a/packages/backend/src/quiz-wizard/comparers/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts index 1f16771..306aabd 100644 --- a/packages/backend/src/quiz-wizard/comparers/test.module.ts +++ b/packages/backend/src/quiz-wizard/comparers/test.module.ts @@ -29,3 +29,14 @@ export async function getCachedDiff(container: string): Promise { return stdoutData; } + +export async function getTreeHead( + container: string, + branch: string, +): Promise { + const { stdoutData } = await executeSSHCommand( + `docker exec -u quizzer -w /home/quizzer/quiz ${container} sh -c "git cat-file -p \\\$(git rev-parse ${branch}) | grep tree | awk '{print \\\$2}'"`, + ); + + return stdoutData; +} diff --git a/packages/backend/src/quiz-wizard/test/comparer4.spec.ts b/packages/backend/src/quiz-wizard/test/comparer4.spec.ts new file mode 100644 index 0000000..80ffad0 --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer4.spec.ts @@ -0,0 +1,32 @@ +import { executeSSHCommand } from '../../common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('4번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(4)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git commit -m "test commit"`, + ); + + expect(await service.submit(containerId, 4)).toBe(true); + }); + + it('전부 다 commit한 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add .`, + `${DOCKER_TEMPLATE} ${containerId} git commit -m "test commit"`, + ); + + expect(await service.submit(containerId, 4)).toBe(false); + }); +}); From a3eb10a8996fca4ae5b5c686368ba5094180b3ee Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 10:44:38 +0900 Subject: [PATCH 045/378] =?UTF-8?q?fix:=20containerId=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EA=B0=92=20=EC=84=A4=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#147] --- packages/backend/src/quizzes/quizzes.controller.ts | 5 ++++- packages/backend/src/session/session.service.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index e425b6b..b50a640 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -106,7 +106,10 @@ export class QuizzesController { id, ); - if (!(await this.containerService.isValidateContainerId(containerId))) { + if ( + !containerId || + !(await this.containerService.isValidateContainerId(containerId)) + ) { this.logger.log( 'info', 'no docker container or invalid container Id. creating container..', diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index efd1346..06ab141 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -37,7 +37,7 @@ export class SessionService { session.problems.set(problemId, { status: 'solving', logs: [], - containerId: 'default-id', + containerId: '', }); this.logger.log('info', `session ${session._id as ObjectId} updated`); this.logger.log( From f353ae5fd5a0dbcdcb6f9f3e84f82c99806a5259 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:52:57 +0900 Subject: [PATCH 046/378] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EB=B0=8F=20console.log=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../backend/src/quiz-wizard/quiz-wizard.service.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts index 1efc6e2..a863b32 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { exec } from 'child_process'; @@ -9,7 +8,6 @@ export class QuizWizardService { async submit(containerId, quizId) { const execAsync = promisify(exec); - console.log('현재 디렉토리 위치:', process.cwd()); try { await execAsync( `yarn run jest ${path.resolve( @@ -25,12 +23,4 @@ export class QuizWizardService { return false; } } - checkDirectoryExists(directoryPath) { - const doesExist = fs.existsSync(directoryPath); - describe('Directory Existence Check', () => { - it('should check if a directory exists', () => { - expect(doesExist).toBe(true); - }); - }); - } } From 04048d9d7fe69ce11118f4a18bccd5aebd0f5ba2 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:53:19 +0900 Subject: [PATCH 047/378] =?UTF-8?q?fix:=20=EB=B6=80=EC=A0=95=ED=99=95?= =?UTF-8?q?=ED=95=9C=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 분명 같이 작업해서 괜찮은 줄 알았어요... [#145] --- packages/backend/src/quiz-wizard/tests/test.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/tests/test.module.ts index e681597..1faadbe 100644 --- a/packages/backend/src/quiz-wizard/tests/test.module.ts +++ b/packages/backend/src/quiz-wizard/tests/test.module.ts @@ -5,7 +5,7 @@ export async function isDirectoryExist( path: string, ): Promise { const { stdoutData } = await executeSSHCommand( - `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/sh "ls -l ${path} | grep ^d"`, + `docker exec -w /home/quizzer/quiz/ -u quizzer ${container} ls -l ${path} | grep ^d`, ); return stdoutData !== ''; From cac9ebe1d69a04563b26a54929a3617a6de8029a Mon Sep 17 00:00:00 2001 From: flydog98 Date: Tue, 28 Nov 2023 23:53:54 +0900 Subject: [PATCH 048/378] =?UTF-8?q?test:=20containerService=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=AA=A8=ED=82=B9=20=EB=B0=8F=201?= =?UTF-8?q?=EB=B2=88=20=EB=AC=B8=EC=A0=9C=20=EC=B1=84=EC=A0=90=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index a4e5296..50453ce 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -1,18 +1,64 @@ import { Test, TestingModule } from '@nestjs/testing'; import { QuizWizardService } from './quiz-wizard.service'; +import { ContainersService } from '../containers/containers.service'; +import { executeSSHCommand } from '../common/ssh.util'; +import { ConfigService } from '@nestjs/config'; + +const mockConfigService = { + get: jest.fn((key) => { + if (key === 'CONTAINER_GIT_USERNAME') { + return 'quizzer'; + } + }), +}; +const mockLogger = {}; describe('QuizWizardService', () => { let service: QuizWizardService; + let containerService: ContainersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [QuizWizardService], + providers: [ + QuizWizardService, + ContainersService, + ContainersService, + { + provide: ConfigService, + useValue: mockConfigService, // 여기서 mockConfigService는 모킹된 ConfigService + }, + { + provide: 'winston', + useValue: mockLogger, // 여기서 mockLogger는 모킹된 Logger + }, + ], }).compile(); service = module.get(QuizWizardService); + containerService = module.get(ContainersService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('1번 문제 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(1); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('init 이후 정답 받는 경우', async () => { + await executeSSHCommand( + `docker exec -w /home/quizzer/quiz -u quizzer ${containerId} git init`, + ); + + expect(await service.submit(containerId, 1)).toBe(true); + }); + + it('init 하지 않은 경우', async () => { + expect(await service.submit(containerId, 1)).toBe(false); + }); }); }); From 9cc68febd71532c9b4d472838394d08db21ec07f Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:26:08 +0900 Subject: [PATCH 049/378] =?UTF-8?q?config:=20uuid=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/package.json | 6 ++++-- yarn.lock | 11 ++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 9dffae1..72c6456 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", + "jest": "^29.7.0", "mongoose": "^8.0.1", "nest-winston": "^1.9.4", "papaparse": "^5.4.1", @@ -46,9 +47,9 @@ "sqlite3": "^5.1.6", "ssh2": "^1.14.0", "typeorm": "^0.3.17", + "uuid": "^9.0.1", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1", - "jest": "^29.7.0" + "winston-daily-rotate-file": "^4.7.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -62,6 +63,7 @@ "@types/shell-escape": "^0.2.3", "@types/ssh2": "^1", "@types/supertest": "^2.0.12", + "@types/uuid": "^9", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.53.0", diff --git a/yarn.lock b/yarn.lock index aaf2b75..b5f5742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5507,6 +5507,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9": + version: 9.0.7 + resolution: "@types/uuid@npm:9.0.7" + checksum: b329ebd4f9d1d8e08d4f2cc211be4922d70d1149f73d5772630e4a3acfb5170c6d37b3d7a39a0412f1a56e86e8a844c7f297c798b082f90380608bf766688787 + languageName: node + linkType: hard + "@types/validator@npm:^13.7.10": version: 13.11.7 resolution: "@types/validator@npm:13.11.7" @@ -6882,6 +6889,7 @@ __metadata: "@types/shell-escape": "npm:^0.2.3" "@types/ssh2": "npm:^1" "@types/supertest": "npm:^2.0.12" + "@types/uuid": "npm:^9" "@typescript-eslint/eslint-plugin": "npm:^6.0.0" "@typescript-eslint/parser": "npm:^6.0.0" class-transformer: "npm:^0.5.1" @@ -6910,6 +6918,7 @@ __metadata: tsconfig-paths: "npm:^4.2.0" typeorm: "npm:^0.3.17" typescript: "npm:^5.0.0-beta" + uuid: "npm:^9.0.1" winston: "npm:^3.11.0" winston-daily-rotate-file: "npm:^4.7.1" languageName: unknown @@ -17971,7 +17980,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.0": +"uuid@npm:9.0.1, uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: From e49fc82b3a64fe475a72ac008505f749f4e99787 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:38:47 +0900 Subject: [PATCH 050/378] =?UTF-8?q?perf:=20SSH=20=EC=97=B0=EC=86=8D=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20SSH?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=ED=95=9C=20=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=EC=88=98=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 코드 작성 중 시간이 오래 걸리는 거 같아서 지금 태스크가 아니지만 개선해 봤습니다... - 약 2초 정도의 시간을 세이브합니다. [#145] --- packages/backend/src/common/ssh.util.ts | 58 ++++++++++++------- .../src/containers/containers.service.ts | 20 +++---- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/common/ssh.util.ts b/packages/backend/src/common/ssh.util.ts index a76343f..0c554c0 100644 --- a/packages/backend/src/common/ssh.util.ts +++ b/packages/backend/src/common/ssh.util.ts @@ -24,7 +24,7 @@ async function getSSH( return conn; } export async function executeSSHCommand( - command: string, + ...commands: string[] ): Promise<{ stdoutData: string; stderrData: string }> { const conn: Client = await getSSH( process.env.CONTAINER_SSH_HOST, @@ -33,26 +33,42 @@ export async function executeSSHCommand( process.env.CONTAINER_SSH_PASSWORD, ); - return new Promise((resolve, reject) => { - conn.exec(command, (err, stream) => { - if (err) { - reject(new Error('SSH command execution Server error')); - return; - } - let stdoutData = ''; - let stderrData = ''; - stream - .on('close', () => { - conn.end(); - resolve({ stdoutData, stderrData }); - }) - .on('data', (chunk) => { - stdoutData += chunk; - }); + const result = await commands.reduce( + async (prevPromise, command) => { + const accum = await prevPromise; + const { stdoutData, stderrData } = await new Promise<{ + stdoutData: string; + stderrData: string; + }>((resolve, reject) => { + conn.exec(command, (err, stream) => { + if (err) { + reject(new Error('SSH command execution Server error')); + return; + } + let stdoutData = ''; + let stderrData = ''; + stream + .on('close', () => { + resolve({ stdoutData, stderrData }); + }) + .on('data', (chunk) => { + stdoutData += chunk; + }); - stream.stderr.on('data', (chunk) => { - stderrData += chunk; + stream.stderr.on('data', (chunk) => { + stderrData += chunk; + }); + }); }); - }); - }); + + return { + stdoutData: accum.stdoutData + stdoutData, + stderrData: accum.stderrData + stderrData, + }; + }, + Promise.resolve({ stdoutData: '', stderrData: '' }), + ); + + conn.end(); + return result; } diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index 0547b4c..f55a0e8 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -4,6 +4,7 @@ import { Logger } from 'winston'; import { CommandResponseDto } from 'src/quizzes/dto/command-response.dto'; import shellEscape from 'shell-escape'; import { executeSSHCommand } from '../common/ssh.util'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class ContainersService { @@ -61,20 +62,19 @@ export class ContainersService { 'CONTAINER_GIT_USERNAME', ); - const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ -mergemasters/alpine-git:0.2 /bin/sh`; - const { stdoutData } = await executeSSHCommand(createContainerCommand); - const containerId = stdoutData.trim(); + const containerId = uuidv4(); - // TODO: 연속 실행할 때 매번 SSH 하는거 리팩토링 해야 함 + const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \ +--name ${containerId} mergemasters/alpine-git:0.2 /bin/sh`; const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`; - await executeSSHCommand(copyFilesCommand); - const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}`; - await executeSSHCommand(chownCommand); - const coreEditorCommand = `docker exec -w /home/quizzer/quiz/ -u ${user} ${containerId} git config --global core.editor /editor/output.sh`; - await executeSSHCommand(coreEditorCommand); + await executeSSHCommand( + createContainerCommand, + copyFilesCommand, + chownCommand, + coreEditorCommand, + ); return containerId; } From 0b37298edd9f06ec81ead3b0f1c5b0c8a4da9fe4 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 00:41:41 +0900 Subject: [PATCH 051/378] =?UTF-8?q?test:=202=EB=B2=88=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EC=B1=84=EC=A0=90=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index 50453ce..562fb11 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -4,6 +4,8 @@ import { ContainersService } from '../containers/containers.service'; import { executeSSHCommand } from '../common/ssh.util'; import { ConfigService } from '@nestjs/config'; +const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; + const mockConfigService = { get: jest.fn((key) => { if (key === 'CONTAINER_GIT_USERNAME') { @@ -38,7 +40,7 @@ describe('QuizWizardService', () => { containerService = module.get(ContainersService); }); - describe('1번 문제 테스트 코드', () => { + describe('1번 문제 채점 테스트 코드', () => { let containerId: string; beforeEach(async () => { @@ -50,9 +52,7 @@ describe('QuizWizardService', () => { }); it('init 이후 정답 받는 경우', async () => { - await executeSSHCommand( - `docker exec -w /home/quizzer/quiz -u quizzer ${containerId} git init`, - ); + await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); expect(await service.submit(containerId, 1)).toBe(true); }); @@ -61,4 +61,29 @@ describe('QuizWizardService', () => { expect(await service.submit(containerId, 1)).toBe(false); }); }); + + describe('2번 문제 채점 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(2); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, + `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, + ); + + expect(await service.submit(containerId, 2)).toBe(true); + }); + + it('아무 동작 없는 케이스', async () => { + expect(await service.submit(containerId, 2)).toBe(false); + }); + }); }); From ba8e3dd01a85ebbe3b81bb8991686d08662fae15 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:25:55 +0900 Subject: [PATCH 052/378] =?UTF-8?q?fix:=20id=EA=B0=80=20=EC=95=84=EB=8B=88?= =?UTF-8?q?=EB=9D=BC=20name=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=90=98=EC=96=B4=20=EB=B0=9C=EC=83=9D=ED=95=9C=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/src/containers/containers.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/containers/containers.service.ts b/packages/backend/src/containers/containers.service.ts index f55a0e8..a04caf1 100644 --- a/packages/backend/src/containers/containers.service.ts +++ b/packages/backend/src/containers/containers.service.ts @@ -80,7 +80,7 @@ export class ContainersService { } async isValidateContainerId(containerId: string): Promise { - const command = `docker ps -a --filter "id=${containerId}" --format "{{.ID}}"`; + const command = `docker ps -a --filter "name=${containerId}" --format "{{.ID}}"`; const { stdoutData, stderrData } = await executeSSHCommand(command); From ad28e203b0f00df3c04a6588cfeb8ab7401687cf Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:34:13 +0900 Subject: [PATCH 053/378] =?UTF-8?q?feat:=203=EB=B2=88=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EC=B1=84=EC=A0=90=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../quiz-wizard/quiz-wizard.service.spec.ts | 28 +++++++++++++++++++ .../backend/src/quiz-wizard/tests/3.spec.ts | 19 +++++++++++++ .../src/quiz-wizard/tests/test.module.ts | 8 ++++++ 3 files changed, 55 insertions(+) create mode 100644 packages/backend/src/quiz-wizard/tests/3.spec.ts diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts index 562fb11..1af641d 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts @@ -86,4 +86,32 @@ describe('QuizWizardService', () => { expect(await service.submit(containerId, 2)).toBe(false); }); }); + + describe('3번 문제 채점 테스트 코드', () => { + let containerId: string; + + beforeEach(async () => { + containerId = await containerService.getContainer(3); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(true); + }); + + it('전부 다 add한 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(false); + }); + }); }); diff --git a/packages/backend/src/quiz-wizard/tests/3.spec.ts b/packages/backend/src/quiz-wizard/tests/3.spec.ts new file mode 100644 index 0000000..c994274 --- /dev/null +++ b/packages/backend/src/quiz-wizard/tests/3.spec.ts @@ -0,0 +1,19 @@ +import { readGitIndex } from './test.module'; + +const containerId = process.argv[3]; + +describe('QuizWizardService', () => { + it('should be true', async () => { + expect(await readGitIndex(containerId)) + .toMatch(`diff --git a/README.md b/README.md +new file mode 100644 +index 0000000..e69de29 +diff --git a/docs/plan.md b/docs/plan.md +index e69de29..3b18e51 100644 +--- a/docs/plan.md ++++ b/docs/plan.md +@@ -0,0 +1 @@ ++hello world +`); + }); +}); diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/tests/test.module.ts index 1faadbe..e558a42 100644 --- a/packages/backend/src/quiz-wizard/tests/test.module.ts +++ b/packages/backend/src/quiz-wizard/tests/test.module.ts @@ -21,3 +21,11 @@ export async function getConfig( return stdoutData; } + +export async function readGitIndex(container: string): Promise { + const { stdoutData } = await executeSSHCommand( + `docker exec -u quizzer -w /home/quizzer/quiz ${container} git diff --cached`, + ); + + return stdoutData; +} From 09f8616324337b6d508fe8056fc8b0c0b03dc7ce Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 01:37:45 +0900 Subject: [PATCH 054/378] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- .../backend/src/quiz-wizard/{tests => comparers}/1.spec.ts | 0 .../backend/src/quiz-wizard/{tests => comparers}/2.spec.ts | 0 .../backend/src/quiz-wizard/{tests => comparers}/3.spec.ts | 0 .../src/quiz-wizard/{tests => comparers}/test.module.ts | 0 packages/backend/src/quiz-wizard/quiz-wizard.service.ts | 2 +- .../src/quiz-wizard/{ => test}/quiz-wizard.service.spec.ts | 6 +++--- 6 files changed, 4 insertions(+), 4 deletions(-) rename packages/backend/src/quiz-wizard/{tests => comparers}/1.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/2.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/3.spec.ts (100%) rename packages/backend/src/quiz-wizard/{tests => comparers}/test.module.ts (100%) rename packages/backend/src/quiz-wizard/{ => test}/quiz-wizard.service.spec.ts (94%) diff --git a/packages/backend/src/quiz-wizard/tests/1.spec.ts b/packages/backend/src/quiz-wizard/comparers/1.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/1.spec.ts rename to packages/backend/src/quiz-wizard/comparers/1.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/2.spec.ts b/packages/backend/src/quiz-wizard/comparers/2.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/2.spec.ts rename to packages/backend/src/quiz-wizard/comparers/2.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/3.spec.ts b/packages/backend/src/quiz-wizard/comparers/3.spec.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/3.spec.ts rename to packages/backend/src/quiz-wizard/comparers/3.spec.ts diff --git a/packages/backend/src/quiz-wizard/tests/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts similarity index 100% rename from packages/backend/src/quiz-wizard/tests/test.module.ts rename to packages/backend/src/quiz-wizard/comparers/test.module.ts diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts index a863b32..0dc9da5 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.ts +++ b/packages/backend/src/quiz-wizard/quiz-wizard.service.ts @@ -14,7 +14,7 @@ export class QuizWizardService { process.cwd(), 'src', 'quiz-wizard', - 'tests', + 'comparers', `${quizId}.spec.ts`, )} ${containerId}`, ); diff --git a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts similarity index 94% rename from packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts rename to packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts index 1af641d..1551e07 100644 --- a/packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts +++ b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { QuizWizardService } from './quiz-wizard.service'; -import { ContainersService } from '../containers/containers.service'; -import { executeSSHCommand } from '../common/ssh.util'; +import { QuizWizardService } from '../quiz-wizard.service'; +import { ContainersService } from '../../containers/containers.service'; +import { executeSSHCommand } from '../../common/ssh.util'; import { ConfigService } from '@nestjs/config'; const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; From 6893e48ab66888d749f6b768ce9b0fbb5f568fb4 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:05:01 +0900 Subject: [PATCH 055/378] =?UTF-8?q?config:=20jest=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컨테이너 생성하고 SSH 하는 로직이 기본 timeout인 5초보다 긴 경우가 많아서 10초로 했습니다. [#145] --- packages/backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 72c6456..fe63f3e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -94,6 +94,7 @@ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "testTimeout": 10000 } } From d4cdf44c99b04ab966260de45744498c8946f37c Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:05:36 +0900 Subject: [PATCH 056/378] =?UTF-8?q?perf:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=B5=ED=86=B5=20=EC=84=A4=EC=A0=95=20export?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=91?= =?UTF-8?q?=EB=A0=AC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 코드는 테스트가 순차적으로 실행되어서 50초까지도 걸렸음 - 문제 3개 기준 20초 정도로 빠르게 실행 가능 [#145] --- .../src/quiz-wizard/test/comparer1.spec.ts | 25 ++++ .../src/quiz-wizard/test/comparer2.spec.ts | 28 +++++ .../src/quiz-wizard/test/comparer3.spec.ts | 31 +++++ .../test/quiz-wizard.service.spec.ts | 117 ------------------ .../test/quiz-wizard.test.setup.ts | 39 ++++++ 5 files changed, 123 insertions(+), 117 deletions(-) create mode 100644 packages/backend/src/quiz-wizard/test/comparer1.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/comparer2.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/comparer3.spec.ts delete mode 100644 packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts create mode 100644 packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts diff --git a/packages/backend/src/quiz-wizard/test/comparer1.spec.ts b/packages/backend/src/quiz-wizard/test/comparer1.spec.ts new file mode 100644 index 0000000..50bce33 --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer1.spec.ts @@ -0,0 +1,25 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('1번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(1)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('init 이후 정답 받는 경우', async () => { + await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); + + expect(await service.submit(containerId, 1)).toBe(true); + }); + + it('init 하지 않은 경우', async () => { + expect(await service.submit(containerId, 1)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/comparer2.spec.ts b/packages/backend/src/quiz-wizard/test/comparer2.spec.ts new file mode 100644 index 0000000..b7158ba --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer2.spec.ts @@ -0,0 +1,28 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('2번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(2)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, + `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, + ); + + expect(await service.submit(containerId, 2)).toBe(true); + }); + + it('아무 동작 없는 케이스', async () => { + expect(await service.submit(containerId, 2)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/comparer3.spec.ts b/packages/backend/src/quiz-wizard/test/comparer3.spec.ts new file mode 100644 index 0000000..d195186 --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/comparer3.spec.ts @@ -0,0 +1,31 @@ +import { executeSSHCommand } from '../../../src/common/ssh.util'; +import { DOCKER_TEMPLATE, setupTestingModule } from './quiz-wizard.test.setup'; +import { QuizWizardService } from '../quiz-wizard.service'; + +describe('3번 문제 채점 테스트', () => { + let service: QuizWizardService, containerId: string; + + beforeEach(async () => { + ({ service, containerId } = await setupTestingModule(3)); + }); + + afterEach(async () => { + await executeSSHCommand(`docker rm -f ${containerId}`); + }); + + it('베스트 정답 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(true); + }); + + it('전부 다 add한 케이스', async () => { + await executeSSHCommand( + `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, + ); + + expect(await service.submit(containerId, 3)).toBe(false); + }); +}); diff --git a/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts deleted file mode 100644 index 1551e07..0000000 --- a/packages/backend/src/quiz-wizard/test/quiz-wizard.service.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { QuizWizardService } from '../quiz-wizard.service'; -import { ContainersService } from '../../containers/containers.service'; -import { executeSSHCommand } from '../../common/ssh.util'; -import { ConfigService } from '@nestjs/config'; - -const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; - -const mockConfigService = { - get: jest.fn((key) => { - if (key === 'CONTAINER_GIT_USERNAME') { - return 'quizzer'; - } - }), -}; -const mockLogger = {}; - -describe('QuizWizardService', () => { - let service: QuizWizardService; - let containerService: ContainersService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - QuizWizardService, - ContainersService, - ContainersService, - { - provide: ConfigService, - useValue: mockConfigService, // 여기서 mockConfigService는 모킹된 ConfigService - }, - { - provide: 'winston', - useValue: mockLogger, // 여기서 mockLogger는 모킹된 Logger - }, - ], - }).compile(); - - service = module.get(QuizWizardService); - containerService = module.get(ContainersService); - }); - - describe('1번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(1); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('init 이후 정답 받는 경우', async () => { - await executeSSHCommand(`${DOCKER_TEMPLATE} ${containerId} git init`); - - expect(await service.submit(containerId, 1)).toBe(true); - }); - - it('init 하지 않은 경우', async () => { - expect(await service.submit(containerId, 1)).toBe(false); - }); - }); - - describe('2번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(2); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('베스트 정답 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git config user.name "Username"`, - `${DOCKER_TEMPLATE} ${containerId} git config user.email "useremail@email.com"`, - ); - - expect(await service.submit(containerId, 2)).toBe(true); - }); - - it('아무 동작 없는 케이스', async () => { - expect(await service.submit(containerId, 2)).toBe(false); - }); - }); - - describe('3번 문제 채점 테스트 코드', () => { - let containerId: string; - - beforeEach(async () => { - containerId = await containerService.getContainer(3); - }); - - afterEach(async () => { - await executeSSHCommand(`docker rm -f ${containerId}`); - }); - - it('베스트 정답 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md`, - ); - - expect(await service.submit(containerId, 3)).toBe(true); - }); - - it('전부 다 add한 케이스', async () => { - await executeSSHCommand( - `${DOCKER_TEMPLATE} ${containerId} git add README.md docs/plan.md docs/architecture.md`, - ); - - expect(await service.submit(containerId, 3)).toBe(false); - }); - }); -}); diff --git a/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts b/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts new file mode 100644 index 0000000..1b637ff --- /dev/null +++ b/packages/backend/src/quiz-wizard/test/quiz-wizard.test.setup.ts @@ -0,0 +1,39 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizWizardService } from '../quiz-wizard.service'; +import { ContainersService } from '../../containers/containers.service'; +import { ConfigService } from '@nestjs/config'; + +export const DOCKER_TEMPLATE = 'docker exec -w /home/quizzer/quiz -u quizzer'; + +const mockConfigService = { + get: jest.fn((key) => { + if (key === 'CONTAINER_GIT_USERNAME') { + return 'quizzer'; + } + }), +}; +const mockLogger = {}; + +export const setupTestingModule = async (quizId: number) => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizWizardService, + ContainersService, + ContainersService, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: 'winston', + useValue: mockLogger, + }, + ], + }).compile(); + + const service = module.get(QuizWizardService); + const containerService = module.get(ContainersService); + const containerId = await containerService.getContainer(quizId); + + return { service, containerService, containerId }; +}; From 8b71dd92626cfc196185a4a822df3e5986e5cff1 Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 02:37:06 +0900 Subject: [PATCH 057/378] =?UTF-8?q?refactor:=20=EC=A1=B0=EA=B8=88=20?= =?UTF-8?q?=EB=8D=94=20=EC=A0=95=ED=99=95=ED=95=9C=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#145] --- packages/backend/src/quiz-wizard/comparers/3.spec.ts | 4 ++-- packages/backend/src/quiz-wizard/comparers/test.module.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/quiz-wizard/comparers/3.spec.ts b/packages/backend/src/quiz-wizard/comparers/3.spec.ts index c994274..519bcf8 100644 --- a/packages/backend/src/quiz-wizard/comparers/3.spec.ts +++ b/packages/backend/src/quiz-wizard/comparers/3.spec.ts @@ -1,10 +1,10 @@ -import { readGitIndex } from './test.module'; +import { getCachedDiff } from './test.module'; const containerId = process.argv[3]; describe('QuizWizardService', () => { it('should be true', async () => { - expect(await readGitIndex(containerId)) + expect(await getCachedDiff(containerId)) .toMatch(`diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/quiz-wizard/comparers/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts index e558a42..1f16771 100644 --- a/packages/backend/src/quiz-wizard/comparers/test.module.ts +++ b/packages/backend/src/quiz-wizard/comparers/test.module.ts @@ -22,7 +22,7 @@ export async function getConfig( return stdoutData; } -export async function readGitIndex(container: string): Promise { +export async function getCachedDiff(container: string): Promise { const { stdoutData } = await executeSSHCommand( `docker exec -u quizzer -w /home/quizzer/quiz ${container} git diff --cached`, ); From 605200c933ebf111e213877f53075c14031d27eb Mon Sep 17 00:00:00 2001 From: flydog98 Date: Wed, 29 Nov 2023 03:07:17 +0900 Subject: [PATCH 058/378] =?UTF-8?q?fix:=20uuid=20name=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20containerID=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20default=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#147] --- packages/backend/src/session/session.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index 06ab141..efd1346 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -37,7 +37,7 @@ export class SessionService { session.problems.set(problemId, { status: 'solving', logs: [], - containerId: '', + containerId: 'default-id', }); this.logger.log('info', `session ${session._id as ObjectId} updated`); this.logger.log( From 8f580d926508b290677dd31ad49f51f16a60f1a1 Mon Sep 17 00:00:00 2001 From: luizy Date: Wed, 29 Nov 2023 12:14:53 +0900 Subject: [PATCH 059/378] refactor: lint adjust [#15] --- packages/backend/src/quiz-wizard/comparers/test.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/quiz-wizard/comparers/test.module.ts b/packages/backend/src/quiz-wizard/comparers/test.module.ts index e9831a2..306aabd 100644 --- a/packages/backend/src/quiz-wizard/comparers/test.module.ts +++ b/packages/backend/src/quiz-wizard/comparers/test.module.ts @@ -39,4 +39,4 @@ export async function getTreeHead( ); return stdoutData; -} \ No newline at end of file +} From b7f87bfe7deeaf916ae57a2b3b3ee028ff4cdde4 Mon Sep 17 00:00:00 2001 From: luizy Date: Wed, 29 Nov 2023 12:15:18 +0900 Subject: [PATCH 060/378] =?UTF-8?q?feat:=20session=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#15] --- .../src/session/session.controller.spec.ts | 18 ++++++++++++ .../backend/src/session/session.controller.ts | 29 +++++++++++++++++++ .../backend/src/session/session.module.ts | 2 ++ .../backend/src/session/session.service.ts | 8 +++++ 4 files changed, 57 insertions(+) create mode 100644 packages/backend/src/session/session.controller.spec.ts create mode 100644 packages/backend/src/session/session.controller.ts diff --git a/packages/backend/src/session/session.controller.spec.ts b/packages/backend/src/session/session.controller.spec.ts new file mode 100644 index 0000000..59e2f77 --- /dev/null +++ b/packages/backend/src/session/session.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SessionController } from './session.controller'; + +describe('SessionController', () => { + let controller: SessionController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SessionController], + }).compile(); + + controller = module.get(SessionController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/backend/src/session/session.controller.ts b/packages/backend/src/session/session.controller.ts new file mode 100644 index 0000000..e10c0f4 --- /dev/null +++ b/packages/backend/src/session/session.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Delete, Res, UseGuards } from '@nestjs/common'; +import { SessionService } from './session.service'; +import { SessionGuard } from './session.guard'; +import { SessionId } from './session.decorator'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; + +@ApiTags('session') +@Controller('api/v1/session') +export class SessionController { + constructor(private readonly sessionService: SessionService) {} + + @Delete() + @UseGuards(SessionGuard) + @ApiOperation({ summary: '세션을 삭제합니다.' }) + @ApiResponse({ + status: 200, + description: '세션을 삭제합니다.', + }) + async deleteSession( + @SessionId() sessionId: string, + @Res() response: Response, + ) { + response.clearCookie('sessionId'); + this.sessionService.deleteSession(sessionId); + response.end(); + return; + } +} diff --git a/packages/backend/src/session/session.module.ts b/packages/backend/src/session/session.module.ts index ccd2a28..74422cb 100644 --- a/packages/backend/src/session/session.module.ts +++ b/packages/backend/src/session/session.module.ts @@ -3,6 +3,7 @@ import { SessionService } from './session.service'; import { MongooseModule } from '@nestjs/mongoose'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Session, SessionSchema } from './schema/session.schema'; +import { SessionController } from './session.controller'; @Module({ imports: [ @@ -17,5 +18,6 @@ import { Session, SessionSchema } from './schema/session.schema'; ], providers: [SessionService], exports: [SessionService], + controllers: [SessionController], }) export class SessionModule {} diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index efd1346..2ef38ac 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -111,4 +111,12 @@ export class SessionService { private async getSessionById(id: string): Promise { return await this.sessionModel.findById(id); } + + async deleteSession(sessionId: string): Promise { + //soft delete + const session = await this.getSessionById(sessionId); + session.deletedAt = new Date(); + session.save(); + this.logger.log('info', `session ${session._id as ObjectId} deleted`); + } } From 40b840e10aa0cd0eb9861c2e596f0d3b1a5d6c8e Mon Sep 17 00:00:00 2001 From: luizy Date: Wed, 29 Nov 2023 15:21:17 +0900 Subject: [PATCH 061/378] =?UTF-8?q?feat:=20log=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#172] --- .../backend/src/quizzes/quizzes.controller.ts | 20 ++++++------- .../src/session/schema/session.schema.ts | 28 +++++++++++++------ .../backend/src/session/session.service.ts | 11 +++++--- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/quizzes/quizzes.controller.ts b/packages/backend/src/quizzes/quizzes.controller.ts index b50a640..2b09fd0 100644 --- a/packages/backend/src/quizzes/quizzes.controller.ts +++ b/packages/backend/src/quizzes/quizzes.controller.ts @@ -10,6 +10,7 @@ import { Inject, Delete, UseGuards, + NotFoundException, } from '@nestjs/common'; import { ApiTags, @@ -136,10 +137,11 @@ export class QuizzesController { execCommandDto.message, )); } else if (execCommandDto.mode === MODE.EDITOR) { - const recentCommand = await this.sessionService.getRecentLog( - sessionId, - id, - ); + const { mode: recentMode, message: recentMessage } = + await this.sessionService.getRecentLog(sessionId, id); + if (recentMode === MODE.EDITOR) { + throw new NotFoundException('편집기 명령 차례가 아닙니다'); + } const bodyPreview = execCommandDto.message.length > 15 @@ -148,12 +150,12 @@ export class QuizzesController { this.logger.log( 'info', - `running editor command "${recentCommand}" for container ${containerId} with body starts with "${bodyPreview}"`, + `running editor command "${recentMessage}" for container ${containerId} with body starts with "${bodyPreview}"`, ); ({ message, result } = await this.containerService.runEditorCommand( containerId, - recentCommand, + recentMessage, execCommandDto.message, )); } else { @@ -163,11 +165,7 @@ export class QuizzesController { } // 일단 editor일 때도 message를 저장합니다. - this.sessionService.pushLogBySessionId( - execCommandDto.message, - sessionId, - id, - ); + this.sessionService.pushLogBySessionId(execCommandDto, sessionId, id); response.status(HttpStatus.OK).send({ message, diff --git a/packages/backend/src/session/schema/session.schema.ts b/packages/backend/src/session/schema/session.schema.ts index 55451f7..4b3889b 100644 --- a/packages/backend/src/session/schema/session.schema.ts +++ b/packages/backend/src/session/schema/session.schema.ts @@ -1,14 +1,15 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; +const Action = { + Command: 'command', + Editor: 'editor', +} as const; + +export type ActionType = (typeof Action)[keyof typeof Action]; + @Schema({ timestamps: true }) export class Session extends Document { - // @Prop({ required: true }) - // createdAt: Date; - // - // @Prop({ required: true }) - // updatedAt: Date; - @Prop() deletedAt: Date | null; @@ -17,7 +18,15 @@ export class Session extends Document { type: Map, of: { status: { type: String, required: true }, - logs: { type: [String], required: true }, + logs: { + type: [ + { + mode: { type: String, enum: Object.values(Action), required: true }, + message: { type: String, required: true }, + }, + ], + required: true, + }, containerId: { type: String, default: '' }, }, }) @@ -25,7 +34,10 @@ export class Session extends Document { number, { status: string; - logs: string[]; + logs: { + mode: ActionType; + message: string; + }[]; containerId: string; } >; diff --git a/packages/backend/src/session/session.service.ts b/packages/backend/src/session/session.service.ts index 2ef38ac..6de467b 100644 --- a/packages/backend/src/session/session.service.ts +++ b/packages/backend/src/session/session.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Logger } from 'winston'; import { Model } from 'mongoose'; -import { Session } from './schema/session.schema'; +import { ActionType, Session } from './schema/session.schema'; import { ObjectId } from 'typeorm'; @Injectable() @@ -71,7 +71,10 @@ export class SessionService { session.save(); } - async getRecentLog(sessionId: string, problemId: number): Promise { + async getRecentLog( + sessionId: string, + problemId: number, + ): Promise<{ mode: string; message: string }> { const session = await this.getSessionById(sessionId); const problemLogs = session?.problems.get(problemId)?.logs; @@ -83,7 +86,7 @@ export class SessionService { } async pushLogBySessionId( - command: string, + log: { mode: ActionType; message: string }, sessionId: string, problemId: number, ): Promise { @@ -91,7 +94,7 @@ export class SessionService { if (!session.problems.get(problemId)) { throw new Error('problem not found'); } - session.problems.get(problemId).logs.push(command); + session.problems.get(problemId).logs.push(log); session.save(); } From 02bcf74a5c4824f386214e05388e141821e2b4a1 Mon Sep 17 00:00:00 2001 From: YuHyun Date: Tue, 28 Nov 2023 14:51:20 +0900 Subject: [PATCH 062/378] =?UTF-8?q?feat:=20Editor=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20>=20=EC=9E=85=EB=A0=A5=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#130] Co-Authored-By: CHAE --- .../src/components/editor/Editor.css.ts | 14 +++++ .../frontend/src/components/editor/Editor.tsx | 52 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 packages/frontend/src/components/editor/Editor.css.ts create mode 100644 packages/frontend/src/components/editor/Editor.tsx diff --git a/packages/frontend/src/components/editor/Editor.css.ts b/packages/frontend/src/components/editor/Editor.css.ts new file mode 100644 index 0000000..e90a5ed --- /dev/null +++ b/packages/frontend/src/components/editor/Editor.css.ts @@ -0,0 +1,14 @@ +import { style } from "@vanilla-extract/css"; + +export const container = style({ + display: "flex", + flexDirection: "column", + width: "100%", +}); + +export const textarea = style({ + resize: "none", + height: 180, +}); + +export const input = style({}); diff --git a/packages/frontend/src/components/editor/Editor.tsx b/packages/frontend/src/components/editor/Editor.tsx new file mode 100644 index 0000000..12ef072 --- /dev/null +++ b/packages/frontend/src/components/editor/Editor.tsx @@ -0,0 +1,52 @@ +import { ChangeEventHandler, KeyboardEventHandler, useState } from "react"; + +import * as styles from "./Editor.css"; + +type ModeType = "insert" | "command"; + +export function Editor() { + const [mode, setMode] = useState("command"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [inputReadonly, setInputReadonly] = useState(true); + const [inputValue, setInputValue] = useState(""); + + const handleTextareaKeyDown: KeyboardEventHandler = (event) => { + const { key } = event; + + if (isCommandMode(mode)) { + if (key === "i") { + setMode("insert"); + setInputValue("-- INSERT --"); + event.preventDefault(); + } + } + }; + + const handleInputChange: ChangeEventHandler = ({ + target: { value }, + }) => { + setInputValue(value); + }; + + return ( + <> + {isCommandMode(mode) ? "명령모드" : "입력모드"} +
+