Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .claude/rules/web/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ paths:

## いつテストを書く・回すか(トリガー)

### ユニット / コンポーネントテスト(vitest + node:test
### ユニット / コンポーネントテスト(vitest)

- **新規フック追加**: 必ず `*.test.ts` を作成(loading / success / error の 3 パス最低限)
- **既存フックの契約変更**: 戻り値・副作用が変わる場合、既存 `*.test.ts` の assert を見直す
- **payloadBuilders / formMappers の変更**: `payloadBuilders.test.ts` を更新(node:test 経由
- **payloadBuilders / formMappers の変更**: `payloadBuilders.test.ts` を更新(vitest
- **api/client.ts の変更**: `api/client.test.ts` を更新(401 リダイレクト、Cookie 認証の挙動)
- **コンポーネント追加**: ロジックを含むものはテストを追加。表示のみのものは省略可

Expand All @@ -27,7 +27,7 @@ paths:
## 実行コマンド

```bash
make test-web # unit + vitest
make test-web # vitest(unit / コンポーネント)
nix develop --command bash -c "cd web && npm run test:e2e" # E2E(Playwright)
```

Expand All @@ -40,7 +40,7 @@ nix develop --command bash -c "cd web && npx vitest run src/hooks/useDocumentFor

以下をすべて満たして初めて「テスト OK」と判定する:

1. **全 unit / vitest pass**: `make test-web` が exit 0
1. **全 vitest pass**: `make test-web` が exit 0
2. **lint が pass**: `make lint-web` も同時に通ること
3. **build が通る**: `make build-web`(tsc + vite build)が通ること。TypeScript の型エラーが残っていないこと
4. **E2E トリガーに該当する場合は E2E pass**: 上記トリガーリストに該当する変更では `npm run test:e2e` を必ず実行し、全シナリオが green
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ htmlcov/
# Node
node_modules/
dist/
web/.test-dist/
# Playwright E2E の生成物(実行ごとに作られるローカル成果物)
web/test-results/
web/playwright-report/
Expand Down
1 change: 0 additions & 1 deletion .jscpd.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"**/node_modules/**",
"**/.venv/**",
"**/dist/**",
"**/.test-dist/**",
"**/__pycache__/**",
"backend/alembic_migrations/versions/**",
"web/src/**/*.test.{ts,tsx}",
Expand Down
42 changes: 14 additions & 28 deletions backend/tests/auth/test_token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,20 @@
# ── RS256 トークン生成・検証 ──────────────────────────────────────


def test_create_access_token_contains_username() -> None:
token = create_access_token("alice")
payload = jwt.decode(token, _test_public_key, algorithms=["RS256"])

assert payload["sub"] == "alice"


def test_create_access_token_has_expiry() -> None:
token = create_access_token("alice")
payload = jwt.decode(token, _test_public_key, algorithms=["RS256"])

assert "exp" in payload


def test_create_access_token_type_is_access() -> None:
token = create_access_token("alice")
payload = jwt.decode(token, _test_public_key, algorithms=["RS256"])

assert payload["type"] == "access"


def test_create_refresh_token_type_is_refresh() -> None:
token, jti = create_refresh_token("alice")
payload = jwt.decode(token, _test_public_key, algorithms=["RS256"])

assert payload["type"] == "refresh"
assert payload["sub"] == "alice"
assert payload["jti"] == jti
def test_access_and_refresh_token_claims() -> None:
"""アクセス / リフレッシュトークンのクレーム契約(sub / exp / type / jti)を検証する。"""
access_payload = jwt.decode(
create_access_token("alice"), _test_public_key, algorithms=["RS256"]
)
assert access_payload["sub"] == "alice"
assert "exp" in access_payload
assert access_payload["type"] == "access"

refresh_token, jti = create_refresh_token("alice")
refresh_payload = jwt.decode(refresh_token, _test_public_key, algorithms=["RS256"])
assert refresh_payload["sub"] == "alice"
assert refresh_payload["type"] == "refresh"
assert refresh_payload["jti"] == jti


def test_hs256_token_rejected_by_rs256_verification() -> None:
Expand Down
13 changes: 1 addition & 12 deletions backend/tests/test_database_libsql_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import importlib

import pytest
from sqlalchemy import LargeBinary, create_engine
from sqlalchemy.pool import NullPool
from sqlalchemy import LargeBinary


@pytest.fixture()
Expand All @@ -35,13 +34,3 @@ def test_large_binary_bind_processor_passes_bytes_through(libsql_engine):
assert proc is not None
payload = b"%PDF-1.3\n%fake-binary"
assert proc(payload) == payload


def test_standard_sqlite_dialect_unaffected():
"""通常の pysqlite ドライバには影響しない(Binary は元から存在する)。"""
engine = create_engine("sqlite:///:memory:", poolclass=NullPool)
assert engine.dialect.driver == "pysqlite"
# pysqlite は標準で Binary を提供する
dbapi = engine.dialect.dbapi
assert dbapi is not None
assert callable(dbapi.Binary)
23 changes: 8 additions & 15 deletions backend/tests/test_messages.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
from app.core.messages import get_error, get_notification, get_success, load_messages


def test_get_notification_returns_message() -> None:
load_messages()

assert (
get_notification("github_link", "completed") == "GitHub連携が完了しました"
)
assert get_notification("github_link", "failed") == "GitHub連携に失敗しました"

def test_key_lookup_returns_message() -> None:
"""キー引きで messages.json の文言が返ること(アクセサごとに代表 1 件)。

def test_get_error_returns_message_by_key() -> None:
文言の逐語 pin を全キーに置くと文言変更のたびに壊れる割に検出力が無いため、
ここではキー→文言の解決経路だけを確認する。
"""
load_messages()

assert get_error("auth.login_required") == "ログインが必要です。"
assert get_notification("github_link", "completed") == "GitHub連携が完了しました"
# schemas/resume.py の model_validator から直接参照される契約なので明示的に固定する
assert get_error("validation.end_date_required") == "在職中でない場合は終了年月を入力してください。"


def test_get_error_formats_placeholders() -> None:
Expand All @@ -34,12 +33,6 @@ def test_missing_message_key_falls_back_to_key() -> None:
assert get_error("unknown.category.key") == "unknown.category.key"


def test_validation_end_date_required() -> None:
load_messages()

assert get_error("validation.end_date_required") == "在職中でない場合は終了年月を入力してください。"


def test_master_data_placeholder() -> None:
"""master_data は {item}、document は {document} で統一されている。"""
load_messages()
Expand Down
186 changes: 62 additions & 124 deletions backend/tests/test_schemas.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from collections.abc import Callable

import pytest
from app.schemas import (
Client,
Experience,
Project,
ResumeCreate,
)
from app.schemas.resume import ProjectPeriod
from pydantic import ValidationError


Expand Down Expand Up @@ -43,17 +46,6 @@ def experience_payload() -> dict:
}


def test_current_experience_forces_end_date_empty() -> None:
"""在籍中(is_current=True)なら end_date は "" に正規化される。"""
payload = experience_payload()
payload["is_current"] = True
payload["end_date"] = "2024-03"

experience = Experience(**payload)

assert experience.end_date == ""


def test_end_date_is_required_when_not_current() -> None:
payload = experience_payload()
payload["is_current"] = False
Expand Down Expand Up @@ -240,137 +232,83 @@ def test_experience_allows_empty_employee_count_and_capital() -> None:
assert exp.capital == ""


def test_experience_end_date_before_start_date_is_rejected() -> None:
"""経歴: 終了日が開始日より前の場合は422エラーとなること。"""
payload = experience_payload()
payload["start_date"] = "2024-04"
payload["end_date"] = "2021-03"
payload["is_current"] = False

with pytest.raises(ValidationError, match="開始日は終了日より前"):
Experience(**payload)


def test_experience_end_date_equals_start_date_is_accepted() -> None:
"""経歴: 終了日 = 開始日は正常に保存されること。"""
def _build_experience_period(
start_date: str, end_date: str | None, is_current: bool
) -> Experience:
"""経歴の期間を検証する(期間フィールドは Experience 自身が持つ)。"""
payload = experience_payload()
payload["start_date"] = "2024-04"
payload["end_date"] = "2024-04"
payload["is_current"] = False
payload["start_date"] = start_date
payload["end_date"] = end_date
payload["is_current"] = is_current
return Experience(**payload)

exp = Experience(**payload)
assert exp.start_date == "2024-04"
assert exp.end_date == "2024-04"


def test_experience_end_date_after_start_date_is_accepted() -> None:
"""経歴: 終了日 > 開始日は正常に保存されること。"""
payload = experience_payload()
payload["start_date"] = "2021-04"
payload["end_date"] = "2024-03"
payload["is_current"] = False

exp = Experience(**payload)
assert exp.start_date == "2021-04"
assert exp.end_date == "2024-03"


def test_experience_in_progress_end_date_is_normalized_to_empty() -> None:
"""経歴: 在職中(is_current=True)は end_date が "" に正規化されること。

schema 上は str 必須・None 不可。在籍中なら値が入っていても "" に丸める。
"""
payload = experience_payload()
payload["is_current"] = True
payload["end_date"] = "2024-03"

exp = Experience(**payload)
assert exp.end_date == ""

def _build_project_period(
start_date: str, end_date: str | None, is_current: bool
) -> ProjectPeriod:
"""プロジェクトの期間を検証する(フラット日付 → periods[0] 移行経路を通す)。"""
proj = Project.model_validate(
{
"name": "テスト",
"start_date": start_date,
"end_date": end_date,
"is_current": is_current,
"technology_stacks": [],
}
)
return proj.periods[0]

def test_experience_end_date_none_is_rejected() -> None:
"""経歴: end_date に None を渡すと ValidationError になる(str 必須契約)。"""
payload = experience_payload()
payload["is_current"] = True
payload["end_date"] = None

with pytest.raises(ValidationError):
Experience(**payload)
# 経歴・プロジェクトは共通の期間バリデーション契約を持つため、両モデルを同一系列で検証する
_period_builders = pytest.mark.parametrize(
"build_period",
[_build_experience_period, _build_project_period],
ids=["experience", "project"],
)
PeriodBuilder = Callable[[str, str | None, bool], Experience | ProjectPeriod]


def test_project_end_date_before_start_date_is_rejected() -> None:
"""プロジェクト: 終了日が開始日より前の場合はエラーとなること。"""
@_period_builders
def test_end_date_before_start_date_is_rejected(build_period: PeriodBuilder) -> None:
"""終了日が開始日より前の場合は 422 エラーとなること。"""
with pytest.raises(ValidationError, match="開始日は終了日より前"):
Project.model_validate(
{
"name": "テスト",
"start_date": "2024-04",
"end_date": "2021-03",
"is_current": False,
"technology_stacks": [],
}
)
build_period("2024-04", "2021-03", False)


def test_project_end_date_equals_start_date_is_accepted() -> None:
"""プロジェクト: 終了日 = 開始日は正常に保存されること。"""
proj = Project.model_validate(
{
"name": "テスト",
"periods": [{"start_date": "2024-04", "end_date": "2024-04", "is_current": False}],
"technology_stacks": [],
}
)
assert proj.periods[0].end_date == "2024-04"
@_period_builders
def test_end_date_equals_start_date_is_accepted(build_period: PeriodBuilder) -> None:
"""終了日 = 開始日は正常に保存されること。"""
period = build_period("2024-04", "2024-04", False)
assert period.start_date == "2024-04"
assert period.end_date == "2024-04"


def test_project_end_date_after_start_date_is_accepted() -> None:
"""プロジェクト: 終了日 > 開始日は正常に保存されること。"""
proj = Project.model_validate(
{
"name": "テスト",
"periods": [{"start_date": "2021-04", "end_date": "2024-03", "is_current": False}],
"technology_stacks": [],
}
)
assert proj.periods[0].end_date == "2024-03"
@_period_builders
def test_end_date_after_start_date_is_accepted(build_period: PeriodBuilder) -> None:
"""終了日 > 開始日は正常に保存されること。"""
period = build_period("2021-04", "2024-03", False)
assert period.start_date == "2021-04"
assert period.end_date == "2024-03"


def test_project_in_progress_end_date_is_normalized_to_empty() -> None:
"""プロジェクト: 参画中(is_current=True)の期間は end_date が "" に正規化されること。"""
# 値が入っていても is_current=True なら "" に正規化される
proj = Project.model_validate(
{
"name": "テスト",
"periods": [{"start_date": "2021-04", "end_date": "2024-03", "is_current": True}],
"technology_stacks": [],
}
)
assert proj.periods[0].end_date == ""
@_period_builders
@pytest.mark.parametrize("end_date", ["2024-03", ""], ids=["value", "empty"])
def test_in_progress_end_date_is_normalized_to_empty(
build_period: PeriodBuilder, end_date: str
) -> None:
"""在職中/参画中(is_current=True)は end_date が "" に正規化されること。

# 空文字列も当然 OK
proj_empty = Project.model_validate(
{
"name": "テスト",
"periods": [{"start_date": "2021-04", "end_date": "", "is_current": True}],
"technology_stacks": [],
}
)
assert proj_empty.periods[0].end_date == ""
schema 上は str 必須・None 不可。値が入っていても "" に丸める。
"""
period = build_period("2021-04", end_date, True)
assert period.end_date == ""


def test_project_end_date_none_is_rejected() -> None:
"""プロジェクト: end_date に None を渡すと ValidationError になる。"""
@_period_builders
def test_end_date_none_is_rejected(build_period: PeriodBuilder) -> None:
"""end_date に None を渡すと ValidationError になる(str 必須契約)。"""
with pytest.raises(ValidationError):
Project.model_validate(
{
"name": "テスト",
"start_date": "2021-04",
"end_date": None,
"is_current": True,
"technology_stacks": [],
}
)
build_period("2021-04", None, True)


def test_non_it_experience_with_description_and_no_clients() -> None:
Expand Down
Loading
Loading