Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
COMPOSE_FILE = docker/docker-compose.prod.yml
COMPOSE_DEV_FILE = docker/docker-compose.yml

# === ビルドと起動(本番用: 単一コマンドでの起動が要件のためこちらをメインにする) ===
# === ビルドと起動 ===

all: up

.env:
cp .env.example .env

# compose を構築するターゲットは .env がないと失敗する(down/logs 系は不要)
up build rebuild rebuild-clean dev-up dev-build dev-rebuild dev-rebuild-clean dev-migrate: .env

up:
Expand All @@ -20,8 +19,6 @@ build:
down:
docker compose -f $(COMPOSE_FILE) down

# === クリーンアップ ===

clean:
docker compose -f $(COMPOSE_FILE) down -v

Expand All @@ -39,7 +36,7 @@ rebuild-clean:
logs:
docker compose -f $(COMPOSE_FILE) logs -f

# === 開発用(ホットリロード付き。本番用とポート8443を共有するため同時起動不可) ===
# === 開発用 ===

dev-up:
docker compose -f $(COMPOSE_DEV_FILE) up -d
Expand Down Expand Up @@ -70,5 +67,9 @@ dev-logs:
dev-migrate:
docker compose -f $(COMPOSE_DEV_FILE) exec -w /app/backend backend npm run prisma:migrate:dev

dev-deps:
docker run --rm -v "$(CURDIR)":/app -w /app --user "$$(id -u):$$(id -g)" node:24-alpine \
npm ci --workspace=frontend --workspace=shared
Comment thread
taka2162 marked this conversation as resolved.

.PHONY: all up build down clean fclean re rebuild rebuild-clean logs \
dev-up dev-build dev-down dev-clean dev-fclean dev-re dev-rebuild dev-rebuild-clean dev-logs dev-migrate
dev-up dev-build dev-down dev-clean dev-fclean dev-re dev-rebuild dev-rebuild-clean dev-logs dev-migrate dev-deps
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ in branches, reviewed by teammates, and integrated through GitHub pull requests.

PostgreSQL is managed through Prisma migrations. The schema is defined in
`backend/prisma/schema.prisma`. On container start, the backend image runs
`npm run prisma:deploy` to apply pending migrations before `npm run start:dev`
starts the server (see `docker/backend/Dockerfile`).
`npm run prisma:deploy` to apply pending migrations before starting the
server: `npm run start:dev` in development (`docker/backend/Dockerfile`), or
the compiled output via `node dist/main.js` in production
(`docker/backend/Dockerfile.prod`).

```mermaid
erDiagram
Expand Down Expand Up @@ -232,7 +234,7 @@ containers and do not need to be installed on the host.
| -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Docker Engine | 20.10 | Runs the containers and images defined in `docker/docker-compose.yml`. |
| Docker Compose | v2 | Must be the Compose V2 plugin invoked as `docker compose`, not the legacy `docker-compose` binary. Required for the `depends_on: condition: service_healthy` syntax the `backend` service uses to wait on the `postgres` service's `healthcheck`. |
| GNU Make | 3.81 | Optional. Every `make` target below is a thin wrapper around a `docker compose` command. |
| GNU Make | 3.81 | Optional. Every `make` target below is a thin wrapper around a `docker compose` command; run that command directly (see the `Makefile`) if `make` isn't available. |

We developed and verified the project on Docker Engine 29.5.3 with Docker
Compose 5.1.4. Check your installed versions with:
Expand Down
132 changes: 132 additions & 0 deletions docs/frontend/e2e-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# フロントエンド E2E テスト (Playwright)

`frontend/tests/e2e/` にある Playwright テストは、実際に起動したスタック(nginx +
フロントエンド + バックエンド + Postgres)に対して、ブラウザからアプリを操作して
検証する。

## ディレクトリ構成

```text
frontend/tests/e2e/
├── global-setup.ts 起動中のスタックが dev/prod どちらかを検査する
├── helpers/ サインアップ・ルーム操作・コンソール監視などの共通処理
└── specs/ テスト本体
```

`specs/` の内訳:

| ファイル | 検証する範囲 |
|---|---|
| `auth.spec.ts` | サインアップ/サインイン/ログアウト、保護ルートのリダイレクト、パスワード変更 |
| `form-validation.spec.ts` | 各フォームのクライアント側バリデーションとエラー表示 |
| `xss.spec.ts` | 各フォームに入れた HTML/スクリプトが実行されずエスケープされる |
| `sql-injection.spec.ts` | 各フォームに入れた SQL がクエリではなく文字列として扱われる |
| `legal-pages.spec.ts` | プライバシーポリシー/利用規約への導線と内容 |
| `responsive.spec.ts` | desktop/tablet/mobile の各幅で横スクロールが発生しない |
| `console-clean.spec.ts` | 主要な画面遷移でコンソールにエラー・警告が出ない |
| `secure-connection.spec.ts` | ページから出る通信に混在コンテンツ (http/ws) が無い |
| `profile.spec.ts` | ユーザー名変更、アバターの選択・アップロード・拒否 |
| `friends.spec.ts` | ユーザー検索・フレンド申請・承認・削除・オンライン表示 |
| `notifications.spec.ts` | 通知バッジのリアルタイム更新、ルーム招待からの参加 |
| `room-lobby.spec.ts` | ロビー/待機室のリアルタイム同期、チャット、ホスト引き継ぎ |
| `concurrent-users.spec.ts` | 4人が同時にログインして別々の画面を操作する |
| `game-match.spec.ts` | 2人対戦の開始・操作・決着 |
| `game-multiplayer.spec.ts` | 4人対戦の開始と全画面の同期 |
| `game-reconnect.spec.ts` | 対戦中に切断しても再接続で試合に復帰できる |
| `match-history.spec.ts` | 試合結果が統計・戦闘履歴・ランキングに反映される |

### インジェクション系テストの対象フォーム

`xss.spec.ts` と `sql-injection.spec.ts` は、ユーザー入力を受け取る次のフォームを
それぞれ同じ観点で攻撃する。ペイロードは `helpers/payloads.ts` に集約している。

- SignUp / SignIn 画面
- Room 内 Chat
- プロフィール編集モーダル(ユーザー名)
- 設定 > アカウント情報変更(メールアドレス・ユーザー名)
- Room 作成モーダル(ルーム名)

XSS 側は「描画結果」ではなく副作用でスクリプトの実行を検知する。`helpers/payloads.ts` の
`attachXssProbe()` が、インラインハンドラが立てるフラグ・`alert()` の dialog・
ペイロードが要素として組み立てられていないかの 3 点を見る。

SQL インジェクション側は、認証を突破できないこと・入力が文字列として保存され
そのまま表示されることに加え、`DROP TABLE` 系を流し込んだ後もサインアップ/
サインイン/ロビーが動作することまで確認する。

## 前提

- Docker / Docker Compose v2 (`docker compose` コマンドが使えること)
- ホストに Node.js が入っていること(Playwright 自体・実ブラウザの起動はホスト側で行う)

## セットアップ

```bash
cp .env.example .env

# 依存関係のインストール(docker-compose.yml と同じ node:24-alpine イメージで実行する。
# ホストの node バージョンが違うと package-lock.json に毎回差分が出てしまうため、
# ホストで直接 npm install はしないこと)
make dev-deps

# Playwright が使うブラウザ本体をインストール(config は chromium のみ使用)
npx playwright install --with-deps chromium
```

`node_modules` は `docker-compose.yml`/`docker-compose.prod.yml` の各コンテナでは匿名ボリューム
としてマウントされており、image ビルド時に作られたものがコンテナ再起動をまたいで永続化される。
そのためホスト側で生成した `node_modules` はコンテナには反映されないし、その逆もない。
`make dev-deps` はコンテナとは無関係に、ホストのリポジトリへ直接 `node_modules` /
`package-lock.json` を書き込む使い捨てコンテナ実行であり、Playwright テストを動かす
ホスト環境の依存関係を Dockerfile と同じ Node バージョンで揃えるためだけに使う。

## テストの実行

Playwright は既定で **開発用スタック**(`docker/docker-compose.yml`)を自動起動する。
本番相当スタック(`docker-compose.prod.yml`)でテストすると、本番環境にテスト用データが
残ってしまうため。

本番相当スタックがポート 8443 で起動している場合は先に止めておく:

```bash
make down
```

テスト実行:

```bash
cd frontend
npm run test:e2e
```

初回は `docker compose -f ../docker/docker-compose.yml up -d --build` が自動で走るため、
イメージビルドに数分かかる。2回目以降は既に起動しているスタックを再利用する
(`reuseExistingServer: true`)。

本番相当スタックに対して実行したい場合:

```bash
E2E_STACK=prod npm run test:e2e
```

この場合、フロントエンドのコードを変更した後は、再ビルドが走るようにコンテナを
一度落とすか `make build` で作り直してから実行すること。

`global-setup.ts` が起動中のスタックの種類(dev/prod)を実際の応答から判定し、
`E2E_STACK` の指定と食い違っていれば実行前にエラーで止まる。

## 結果の確認

```bash
npm run test:e2e:report # Playwright の HTML レポートを開く
```

## 注意点

- テストは実際に Postgres へユーザー・ルーム・試合結果を書き込む。クリーンアップ処理は
無いため、繰り返し実行するとデータが蓄積する(ユーザー名は毎回一意にしているので
衝突はしない)。気になる場合は `make clean`(dev)または
`docker compose -f docker/docker-compose.prod.yml down -v` でボリュームごと削除してから
再実行する。
- テストの並列実行はしない(`workers: 1`)。ルーム/ゲーム状態はバックエンドの
in-memory `Map` で単一プロセス内に保持されており、並列実行すると互いに干渉するため。
7 changes: 6 additions & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ dist-ssr
*.sln
*.sw?

.vite
.vite

# Playwright
/test-results/
/playwright-report/
/playwright/.cache/
6 changes: 5 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"lint": "eslint .",
"preview": "vite preview",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\""
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
},
"dependencies": {
"@ft_transcendence/shared": "*",
Expand All @@ -27,6 +30,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.62.1",
"@tailwindcss/postcss": "^4.3.1",
"@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.12.2",
Expand Down
59 changes: 59 additions & 0 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { defineConfig, devices } from '@playwright/test'

// 本番用 compose(docker-compose.prod.yml)でテストすると、本番環境にテスト用データが
// 残ってしまう。既定は dev。`E2E_STACK=prod` を指定すれば本番用に切り替えられる
// (両方とも 8443 を使うため同時起動は不可)。
const STACK = process.env.E2E_STACK === 'prod' ? 'prod' : 'dev'

const COMPOSE_FILE =
STACK === 'dev'
? '../docker/docker-compose.yml'
: '../docker/docker-compose.prod.yml'

// prod は静的ビルド成果物を配信するため、フロントの変更を反映するには再ビルドがいる。
const WEB_SERVER_COMMAND = `docker compose -f ${COMPOSE_FILE} up --build`

export default defineConfig({
testDir: './tests/e2e',
// ルーム/ゲーム状態はバックエンドの in-memory Map(単一プロセス)で共有されているため、
// テストを並列実行すると互いに干渉する。安定性優先で直列実行する。
fullyParallel: false,
workers: 1,
retries: 0,
// 実サインアップ・実ソケット接続・複数ブラウザコンテキストをまたぐシナリオが多く、
// デフォルトの 30s では余裕が無いテストがある(特に 4人対戦や複数コンテキストの同期)。
timeout: 60_000,
globalSetup: './tests/e2e/global-setup.ts',
webServer: {
command: WEB_SERVER_COMMAND,
url: 'https://localhost:8443',
ignoreHTTPSErrors: true,
reuseExistingServer: true,
timeout: 180_000,
},
use: {
baseURL: 'https://localhost:8443',
ignoreHTTPSErrors: true,
trace: 'retain-on-failure',
video: 'retain-on-failure',
screenshot: 'only-on-failure',
},
reporter: [['html', { open: 'never' }], ['list']],
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
launchOptions: {
// headless Chromium ではソフトウェア GL が無効な環境があり、R3F(WebGL)の
// canvas が真っ黒/未描画になることがある。SwiftShader を強制して描画させる。
args: [
'--use-gl=angle',
'--use-angle=swiftshader',
'--ignore-gpu-blocklist',
],
},
},
},
],
})
2 changes: 1 addition & 1 deletion frontend/src/pages/LobbyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function LobbyPage() {
</p>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<div className="ml-auto flex items-center gap-2 sm:gap-3">
<button
onClick={() => navigate('/home')}
className="rounded-full border border-cyan-700/60 bg-black/30 px-3 sm:px-5 py-2 text-xs font-bold tracking-widest text-cyan-300 hover:border-cyan-400 hover:text-white transition-colors whitespace-nowrap"
Expand Down
47 changes: 47 additions & 0 deletions frontend/tests/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { FullConfig } from '@playwright/test'
import https from 'node:https'

const STACK = process.env.E2E_STACK === 'prod' ? 'prod' : 'dev'

function fetchHtml(url: string): Promise<string> {
return new Promise((resolve, reject) => {
https
.get(url, { rejectUnauthorized: false }, (res) => {
let body = ''
res.on('data', (chunk: Buffer) => (body += chunk.toString()))
res.on('end', () => resolve(body))
})
.on('error', reject)
})
}

/**
* dev 用 compose は Vite の HMR クライアントスクリプトを埋め込むため、
* 「コンソールに警告・エラーが無いこと」を検査するテストが dev と prod で別結果になる。
*
* webServer(playwright.config.ts)は既に起動済み・応答済みのはずだが、ポート 8443 を
* 手動で別スタックが握っていた場合(reuseExistingServer: true のため検知できない)、
* 誤ったスタックをそのまま検査してしまう事故が起きる。ここで応答 HTML から実際の
* スタック種別を判別し、E2E_STACK の指定と食い違っていれば即座に失敗させる。
*/
export default async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL ?? 'https://localhost:8443'
const html = await fetchHtml(baseURL)

// dev: Vite が index.html にソースを直接埋め込む (`/src/main.tsx`)
// prod: nginx が vite build のハッシュ付きバンドルを配信する (`/assets/index-XXXX.js`)
const looksLikeDev = html.includes('/src/main.tsx')
const looksLikeProd = /\/assets\/index-[\w-]+\.js/.test(html)
const actualStack = looksLikeDev ? 'dev' : looksLikeProd ? 'prod' : 'unknown'

if (actualStack !== STACK) {
const fix =
STACK === 'prod'
? '`make dev-down` で開発用スタックを止めてから、`E2E_STACK=prod npx playwright test` を実行してください。'
: '`make down` で本番スタックを止めてから、`npx playwright test`(E2E_STACK は既定で dev)を実行してください。'

throw new Error(
`E2E_STACK=${STACK} を要求しましたが、${baseURL} は "${actualStack}" スタックの応答でした。\n${fix}`
)
}
}
Loading
Loading