Skip to main content

🔐 Phase 2: 認証・セキュリティテスト実装

📋 実装概要

MVPでのサムネイル表示問題を防ぐための重要テストフェーズ。Firebase App Check統合確認。

必須成功率: 100%

認証・セキュリティは妥協できません

🛡️ Firebase App Check テスト

Web reCAPTCHA統合テスト

// lib/test-engine/phase2/app-check-tests.ts
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaV3Provider, getToken } from 'firebase/app-check';
import { TestResult } from '../types';

export class AppCheckTests {
private app: any;
private appCheck: any;
private recaptchaSiteKey: string;

constructor(firebaseConfig: any, recaptchaSiteKey: string) {
this.app = initializeApp(firebaseConfig);
this.recaptchaSiteKey = recaptchaSiteKey;
}

async testRecaptchaInitialization(): Promise<TestResult> {
const startTime = Date.now();
const testId = `recaptcha-init-${Date.now()}`;

try {
console.log('🔍 reCAPTCHA初期化テスト開始...');

// App Check初期化
this.appCheck = initializeAppCheck(this.app, {
provider: new ReCaptchaV3Provider(this.recaptchaSiteKey),
isTokenAutoRefreshEnabled: true
});

// reCAPTCHAスクリプト読み込み確認
const recaptchaScript = document.querySelector('script[src*="recaptcha"]');
if (!recaptchaScript) {
throw new Error('reCAPTCHAスクリプトが読み込まれていません');
}

// window.grecaptchaの存在確認
if (typeof window !== 'undefined' && !window.grecaptcha) {
// 少し待ってから再確認
await new Promise(resolve => setTimeout(resolve, 2000));
if (!window.grecaptcha) {
throw new Error('grecaptcha オブジェクトが利用できません');
}
}

const duration = Date.now() - startTime;
console.log(`✅ reCAPTCHA初期化成功: ${duration}ms`);

return {
id: testId,
name: 'reCAPTCHA初期化',
status: 'success',
message: `初期化成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
siteKey: this.recaptchaSiteKey,
scriptLoaded: !!recaptchaScript,
grecaptchaAvailable: typeof window !== 'undefined' ? !!window.grecaptcha : 'server-side',
autoRefresh: true
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ reCAPTCHA初期化エラー:', error);

return {
id: testId,
name: 'reCAPTCHA初期化',
status: 'error',
message: `初期化失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error,
details: {
errorType: 'initialization_failed',
siteKey: this.recaptchaSiteKey
}
};
}
}

async testTokenGeneration(): Promise<TestResult> {
const startTime = Date.now();
const testId = `appcheck-token-${Date.now()}`;

try {
console.log('🔍 App Checkトークン生成テスト開始...');

if (!this.appCheck) {
throw new Error('App Checkが初期化されていません');
}

// トークン生成(強制更新)
const tokenResult = await getToken(this.appCheck, true);

if (!tokenResult.token) {
throw new Error('トークンが生成されませんでした');
}

// トークンの基本検証
const tokenParts = tokenResult.token.split('.');
if (tokenParts.length !== 3) {
throw new Error('無効なJWTトークン形式');
}

// JWTヘッダー解析
const header = JSON.parse(atob(tokenParts[0]));
if (header.typ !== 'JWT') {
throw new Error('JWTヘッダーが無効');
}

// JWTペイロード解析
const payload = JSON.parse(atob(tokenParts[1]));
const now = Math.floor(Date.now() / 1000);

if (payload.exp <= now) {
throw new Error('トークンが既に期限切れ');
}

const duration = Date.now() - startTime;
console.log(`✅ App Checkトークン生成成功: ${duration}ms`);

return {
id: testId,
name: 'App Checkトークン生成',
status: 'success',
message: `トークン生成成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
tokenLength: tokenResult.token.length,
header,
payload: {
iss: payload.iss,
aud: payload.aud,
exp: payload.exp,
iat: payload.iat,
sub: payload.sub
},
expiresAt: new Date(payload.exp * 1000),
validDuration: payload.exp - now
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ App Checkトークン生成エラー:', error);

return {
id: testId,
name: 'App Checkトークン生成',
status: 'error',
message: `トークン生成失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testTokenVerification(apiEndpoint: string): Promise<TestResult> {
const startTime = Date.now();
const testId = `appcheck-verify-${Date.now()}`;

try {
console.log('🔍 App Checkトークン検証テスト開始...');

if (!this.appCheck) {
throw new Error('App Checkが初期化されていません');
}

// 新しいトークン取得
const tokenResult = await getToken(this.appCheck);

// API呼び出し(トークン付き)
const response = await fetch(`${apiEndpoint}/api/debug/app-check-verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Firebase-AppCheck': tokenResult.token
},
body: JSON.stringify({
testData: 'app-check-verification',
timestamp: new Date().toISOString()
})
});

if (!response.ok) {
throw new Error(`API検証失敗: HTTP ${response.status} ${response.statusText}`);
}

const verificationResult = await response.json();

if (!verificationResult.success) {
throw new Error(`サーバー側検証失敗: ${verificationResult.error}`);
}

const duration = Date.now() - startTime;
console.log(`✅ App Checkトークン検証成功: ${duration}ms`);

return {
id: testId,
name: 'App Checkトークン検証',
status: 'success',
message: `トークン検証成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
apiEndpoint,
verificationResult,
tokenUsed: tokenResult.token.substring(0, 50) + '...',
serverValidation: verificationResult
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ App Checkトークン検証エラー:', error);

return {
id: testId,
name: 'App Checkトークン検証',
status: 'error',
message: `トークン検証失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testImageUploadWithAppCheck(uploadEndpoint: string): Promise<TestResult> {
const startTime = Date.now();
const testId = `image-upload-appcheck-${Date.now()}`;

try {
console.log('🔍 App Check付き画像アップロードテスト開始...');

// テスト用画像生成(1x1ピクセルPNG)
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 1, 1);

// Blobに変換
const imageBlob = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/png');
});

// App Checkトークン取得
const tokenResult = await getToken(this.appCheck);

// FormData作成
const formData = new FormData();
formData.append('image', imageBlob, 'test-image.png');
formData.append('brand', 'neko');
formData.append('testMode', 'true');

// アップロード実行
const response = await fetch(uploadEndpoint, {
method: 'POST',
headers: {
'X-Firebase-AppCheck': tokenResult.token
},
body: formData
});

if (!response.ok) {
throw new Error(`アップロード失敗: HTTP ${response.status}`);
}

const uploadResult = await response.json();

if (!uploadResult.success) {
throw new Error(`アップロード処理失敗: ${uploadResult.error}`);
}

// サムネイル表示確認
const thumbnailUrl = uploadResult.thumbnailUrl;
if (!thumbnailUrl) {
throw new Error('サムネイルURLが取得できません');
}

// サムネイル読み込みテスト
const thumbnailLoadStart = performance.now();
const thumbnailResponse = await fetch(thumbnailUrl);
const thumbnailLoadTime = Math.round(performance.now() - thumbnailLoadStart);

if (!thumbnailResponse.ok) {
throw new Error(`サムネイル読み込み失敗: HTTP ${thumbnailResponse.status}`);
}

const duration = Date.now() - startTime;
console.log(`✅ App Check付き画像アップロード成功: ${duration}ms`);

return {
id: testId,
name: 'App Check付き画像アップロード',
status: 'success',
message: `アップロード・サムネイル表示成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
uploadResult,
thumbnailUrl,
thumbnailLoadTime,
imageSize: imageBlob.size,
appCheckUsed: true,
thumbnailAccessible: thumbnailResponse.ok
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ App Check付き画像アップロードエラー:', error);

return {
id: testId,
name: 'App Check付き画像アップロード',
status: 'error',
message: `アップロード失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

🔐 Cloudflare Access認証テスト

セッション管理・権限確認テスト

// lib/test-engine/phase2/cloudflare-access-tests.ts
export class CloudflareAccessTests {
private baseUrl: string;

constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}

async testAccessAuthentication(): Promise<TestResult> {
const startTime = Date.now();
const testId = `cf-access-auth-${Date.now()}`;

try {
console.log('🔍 Cloudflare Access認証テスト開始...');

// 保護されたエンドポイントにアクセス
const response = await fetch(`${this.baseUrl}/api/admin/protected`, {
method: 'GET',
headers: {
'User-Agent': 'Debug-Test-Suite/1.0'
},
credentials: 'include' // Cookieを含める
});

// Cloudflare Accessヘッダー確認
const cfEmail = response.headers.get('CF-Access-Authenticated-User-Email');
const cfGroups = response.headers.get('CF-Access-Authenticated-User-Groups');
const cfUserId = response.headers.get('CF-Access-Authenticated-User-ID');

if (response.status === 401 || response.status === 403) {
// 認証されていない場合は期待される動作
return {
id: testId,
name: 'Cloudflare Access認証',
status: 'warning',
message: 'Access認証が必要(期待される動作)',
startTime: new Date(startTime),
endTime: new Date(),
duration: Date.now() - startTime,
details: {
status: response.status,
redirectUrl: response.headers.get('Location'),
requiresAuth: true
}
};
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const authData = await response.json();
const duration = Date.now() - startTime;

console.log(`✅ Cloudflare Access認証確認: ${duration}ms`);

return {
id: testId,
name: 'Cloudflare Access認証',
status: 'success',
message: `認証済み (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
cfEmail,
cfGroups,
cfUserId,
authData,
authenticated: true
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Cloudflare Access認証エラー:', error);

return {
id: testId,
name: 'Cloudflare Access認証',
status: 'error',
message: `認証テスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testSessionManagement(): Promise<TestResult> {
const startTime = Date.now();
const testId = `cf-session-mgmt-${Date.now()}`;

try {
console.log('🔍 Cloudflare Accessセッション管理テスト開始...');

// セッション情報取得
const response = await fetch(`${this.baseUrl}/api/admin/session`, {
method: 'GET',
credentials: 'include'
});

if (response.status === 401) {
return {
id: testId,
name: 'Cloudflare Accessセッション管理',
status: 'warning',
message: 'セッションなし(認証が必要)',
startTime: new Date(startTime),
endTime: new Date(),
duration: Date.now() - startTime,
details: { requiresAuth: true }
};
}

if (!response.ok) {
throw new Error(`セッション取得失敗: HTTP ${response.status}`);
}

const sessionData = await response.json();
const duration = Date.now() - startTime;

// セッション有効期限チェック
const expiresAt = new Date(sessionData.expiresAt);
const now = new Date();
const timeUntilExpiry = expiresAt.getTime() - now.getTime();
const hoursUntilExpiry = timeUntilExpiry / (1000 * 60 * 60);

const status = hoursUntilExpiry < 1 ? 'warning' : 'success';
const message = hoursUntilExpiry < 1
? `セッション期限切れ間近 (${hoursUntilExpiry.toFixed(1)}時間)`
: `セッション有効 (残り${hoursUntilExpiry.toFixed(1)}時間)`;

console.log(`✅ Cloudflare Accessセッション確認: ${duration}ms`);

return {
id: testId,
name: 'Cloudflare Accessセッション管理',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
sessionData,
expiresAt,
hoursUntilExpiry: hoursUntilExpiry.toFixed(1),
sessionValid: timeUntilExpiry > 0
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Cloudflare Accessセッション管理エラー:', error);

return {
id: testId,
name: 'Cloudflare Accessセッション管理',
status: 'error',
message: `セッション管理テスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testPermissionValidation(): Promise<TestResult> {
const startTime = Date.now();
const testId = `cf-permission-${Date.now()}`;

try {
console.log('🔍 Cloudflare Access権限検証テスト開始...');

// 管理者権限が必要なエンドポイント群テスト
const permissionTests = [
{ endpoint: '/api/admin/system-config', permission: 'system_admin' },
{ endpoint: '/api/admin/users', permission: 'user_management' },
{ endpoint: '/api/admin/brands', permission: 'brand_management' },
{ endpoint: '/api/admin/debug', permission: 'debug_access' }
];

const permissionResults = [];

for (const test of permissionTests) {
try {
const response = await fetch(`${this.baseUrl}${test.endpoint}`, {
method: 'GET',
credentials: 'include'
});

permissionResults.push({
endpoint: test.endpoint,
permission: test.permission,
status: response.status,
hasAccess: response.ok,
message: response.ok ? 'アクセス許可' : `アクセス拒否 (${response.status})`
});

} catch (error) {
permissionResults.push({
endpoint: test.endpoint,
permission: test.permission,
status: 'error',
hasAccess: false,
message: `エラー: ${error.message}`
});
}
}

const accessibleCount = permissionResults.filter(r => r.hasAccess).length;
const duration = Date.now() - startTime;

// 完全にアクセス不可の場合は認証されていない
const status = accessibleCount === 0 ? 'warning' : 'success';
const message = accessibleCount === 0
? '認証が必要(権限なし)'
: `権限確認完了 (${accessibleCount}/${permissionResults.length}件アクセス可能)`;

console.log(`✅ Cloudflare Access権限検証: ${duration}ms`);

return {
id: testId,
name: 'Cloudflare Access権限検証',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
permissionResults,
accessibleEndpoints: accessibleCount,
totalEndpoints: permissionResults.length,
accessRate: (accessibleCount / permissionResults.length * 100).toFixed(1)
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Cloudflare Access権限検証エラー:', error);

return {
id: testId,
name: 'Cloudflare Access権限検証',
status: 'error',
message: `権限検証失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

🔒 セキュリティ設定テスト

CORS・CSP・セキュリティヘッダーテスト

// lib/test-engine/phase2/security-tests.ts
export class SecurityTests {
private baseUrl: string;

constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}

async testCORSConfiguration(): Promise<TestResult> {
const startTime = Date.now();
const testId = `cors-config-${Date.now()}`;

try {
console.log('🔍 CORS設定テスト開始...');

// プリフライトリクエストテスト
const preflightResponse = await fetch(`${this.baseUrl}/api/uploads`, {
method: 'OPTIONS',
headers: {
'Origin': 'https://app.contents-print.jp',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type, X-Firebase-AppCheck'
}
});

const corsHeaders = {
allowOrigin: preflightResponse.headers.get('Access-Control-Allow-Origin'),
allowMethods: preflightResponse.headers.get('Access-Control-Allow-Methods'),
allowHeaders: preflightResponse.headers.get('Access-Control-Allow-Headers'),
allowCredentials: preflightResponse.headers.get('Access-Control-Allow-Credentials'),
maxAge: preflightResponse.headers.get('Access-Control-Max-Age')
};

// CORS設定検証
const issues = [];

if (!corsHeaders.allowOrigin) {
issues.push('Access-Control-Allow-Originが設定されていません');
}

if (!corsHeaders.allowMethods?.includes('POST')) {
issues.push('POSTメソッドが許可されていません');
}

if (!corsHeaders.allowHeaders?.includes('X-Firebase-AppCheck')) {
issues.push('X-Firebase-AppCheckヘッダーが許可されていません');
}

if (!corsHeaders.allowCredentials) {
issues.push('認証情報が許可されていません');
}

const duration = Date.now() - startTime;
const status = issues.length === 0 ? 'success' : 'warning';
const message = issues.length === 0
? `CORS設定正常 (${duration}ms)`
: `CORS設定に問題: ${issues.length}`;

console.log(`✅ CORS設定確認: ${duration}ms`);

return {
id: testId,
name: 'CORS設定',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
corsHeaders,
issues,
preflightStatus: preflightResponse.status
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ CORS設定テストエラー:', error);

return {
id: testId,
name: 'CORS設定',
status: 'error',
message: `CORS設定テスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testSecurityHeaders(): Promise<TestResult> {
const startTime = Date.now();
const testId = `security-headers-${Date.now()}`;

try {
console.log('🔍 セキュリティヘッダーテスト開始...');

const response = await fetch(`${this.baseUrl}/api/health`);

// 重要なセキュリティヘッダー確認
const securityHeaders = {
contentSecurityPolicy: response.headers.get('Content-Security-Policy'),
strictTransportSecurity: response.headers.get('Strict-Transport-Security'),
xFrameOptions: response.headers.get('X-Frame-Options'),
xContentTypeOptions: response.headers.get('X-Content-Type-Options'),
referrerPolicy: response.headers.get('Referrer-Policy'),
permissionsPolicy: response.headers.get('Permissions-Policy')
};

// セキュリティヘッダー評価
const evaluations = [
{
header: 'Content-Security-Policy',
value: securityHeaders.contentSecurityPolicy,
required: true,
evaluation: securityHeaders.contentSecurityPolicy ? 'present' : 'missing'
},
{
header: 'Strict-Transport-Security',
value: securityHeaders.strictTransportSecurity,
required: true,
evaluation: securityHeaders.strictTransportSecurity ? 'present' : 'missing'
},
{
header: 'X-Frame-Options',
value: securityHeaders.xFrameOptions,
required: true,
evaluation: securityHeaders.xFrameOptions ? 'present' : 'missing'
},
{
header: 'X-Content-Type-Options',
value: securityHeaders.xContentTypeOptions,
required: true,
evaluation: securityHeaders.xContentTypeOptions === 'nosniff' ? 'correct' : 'incorrect'
}
];

const missingRequired = evaluations.filter(e => e.required && e.evaluation === 'missing');
const incorrectValues = evaluations.filter(e => e.evaluation === 'incorrect');

const duration = Date.now() - startTime;
const issues = missingRequired.length + incorrectValues.length;
const status = issues === 0 ? 'success' : 'warning';
const message = issues === 0
? `セキュリティヘッダー正常 (${duration}ms)`
: `セキュリティヘッダー問題: ${issues}`;

console.log(`✅ セキュリティヘッダー確認: ${duration}ms`);

return {
id: testId,
name: 'セキュリティヘッダー',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
securityHeaders,
evaluations,
missingRequired,
incorrectValues,
score: ((evaluations.length - issues) / evaluations.length * 100).toFixed(1)
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ セキュリティヘッダーテストエラー:', error);

return {
id: testId,
name: 'セキュリティヘッダー',
status: 'error',
message: `セキュリティヘッダーテスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testRateLimiting(): Promise<TestResult> {
const startTime = Date.now();
const testId = `rate-limiting-${Date.now()}`;

try {
console.log('🔍 レート制限テスト開始...');

const testEndpoint = `${this.baseUrl}/api/debug/rate-limit-test`;
const maxRequests = 100; // 1分間の制限テスト
const results = [];

// 連続リクエスト実行
for (let i = 0; i < maxRequests; i++) {
const requestStart = performance.now();

try {
const response = await fetch(testEndpoint, {
method: 'GET',
headers: {
'User-Agent': `Debug-Test-Suite/1.0 (Request ${i + 1})`
}
});

const requestTime = Math.round(performance.now() - requestStart);

results.push({
requestNumber: i + 1,
status: response.status,
responseTime: requestTime,
rateLimited: response.status === 429,
retryAfter: response.headers.get('Retry-After')
});

// レート制限に達したら終了
if (response.status === 429) {
console.log(`レート制限検出: ${i + 1}リクエスト目`);
break;
}

// 少し間隔を開ける(サーバー負荷軽減)
if (i % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}

} catch (requestError) {
results.push({
requestNumber: i + 1,
status: 'error',
responseTime: Math.round(performance.now() - requestStart),
error: (requestError as Error).message
});
}
}

const rateLimitedCount = results.filter(r => r.rateLimited).length;
const errorCount = results.filter(r => r.status === 'error').length;
const successCount = results.length - rateLimitedCount - errorCount;

const duration = Date.now() - startTime;

// レート制限の評価
const status = rateLimitedCount > 0 ? 'success' : 'warning';
const message = rateLimitedCount > 0
? `レート制限正常動作 (${successCount}件成功後制限)`
: `レート制限未検出 (${successCount}件成功)`;

console.log(`✅ レート制限テスト完了: ${duration}ms`);

return {
id: testId,
name: 'レート制限',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
totalRequests: results.length,
successCount,
rateLimitedCount,
errorCount,
averageResponseTime: results.reduce((sum, r) => sum + r.responseTime, 0) / results.length,
firstRateLimitAt: rateLimitedCount > 0 ? results.find(r => r.rateLimited)?.requestNumber : null,
results: results.slice(0, 10) // 最初の10件のみ詳細記録
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ レート制限テストエラー:', error);

return {
id: testId,
name: 'レート制限',
status: 'error',
message: `レート制限テスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

🧪 Phase 2 統合実行エンジン

認証・セキュリティテスト統合実行

// lib/test-engine/phase2/index.ts
import { AppCheckTests } from './app-check-tests';
import { CloudflareAccessTests } from './cloudflare-access-tests';
import { SecurityTests } from './security-tests';
import { TestResult, TestSuite } from '../types';

export class Phase2TestRunner {
private firebaseConfig: any;
private recaptchaSiteKey: string;
private baseUrl: string;

constructor(config: {
firebaseConfig: any;
recaptchaSiteKey: string;
baseUrl: string;
}) {
this.firebaseConfig = config.firebaseConfig;
this.recaptchaSiteKey = config.recaptchaSiteKey;
this.baseUrl = config.baseUrl;
}

async executeAll(): Promise<TestSuite> {
const startTime = new Date();
console.log('🔐 Phase 2: 認証・セキュリティテスト開始');

const appCheckTests = new AppCheckTests(this.firebaseConfig, this.recaptchaSiteKey);
const accessTests = new CloudflareAccessTests(this.baseUrl);
const securityTests = new SecurityTests(this.baseUrl);

const allTests: TestResult[] = [];

try {
// Firebase App Checkテスト(順次実行 - 依存関係あり)
console.log('🛡️ Firebase App Checkテスト...');
const appCheckResults = [
await appCheckTests.testRecaptchaInitialization(),
await appCheckTests.testTokenGeneration(),
await appCheckTests.testTokenVerification(this.baseUrl),
await appCheckTests.testImageUploadWithAppCheck(`${this.baseUrl}/api/uploads`)
];
allTests.push(...appCheckResults);

// Cloudflare Accessテスト(並列実行可能)
console.log('🔐 Cloudflare Accessテスト...');
const accessResults = await Promise.all([
accessTests.testAccessAuthentication(),
accessTests.testSessionManagement(),
accessTests.testPermissionValidation()
]);
allTests.push(...accessResults);

// セキュリティ設定テスト(並列実行可能)
console.log('🔒 セキュリティ設定テスト...');
const securityResults = await Promise.all([
securityTests.testCORSConfiguration(),
securityTests.testSecurityHeaders(),
securityTests.testRateLimiting()
]);
allTests.push(...securityResults);

} catch (error) {
console.error('❌ Phase 2テスト実行エラー:', error);
}

const endTime = new Date();
const successCount = allTests.filter(t => t.status === 'success').length;
const warningCount = allTests.filter(t => t.status === 'warning').length;
const errorCount = allTests.filter(t => t.status === 'error').length;
const successRate = (successCount / allTests.length) * 100;

const overallStatus = errorCount > 0 ? 'error' :
warningCount > 0 ? 'warning' : 'success';

const suite: TestSuite = {
id: `phase2-${Date.now()}`,
name: 'Phase 2: 認証・セキュリティ',
phase: 2,
priority: 'critical',
requiredSuccessRate: 100,
tests: allTests,
overallStatus,
startTime,
endTime,
successRate: Math.round(successRate)
};

console.log(`🏁 Phase 2完了: ${successRate.toFixed(1)}% (${successCount}/${allTests.length})`);

// App Check関連テストの成功確認(サムネイル問題対策)
const appCheckSuccess = appCheckResults.filter(r => r.status === 'success').length;
if (appCheckSuccess < 3) { // 最低3つ(初期化、トークン生成、検証)
console.warn('⚠️ App Checkテスト不完全: サムネイル表示問題のリスクあり');
}

return suite;
}

// 個別テスト実行メソッド
async testAppCheckOnly(): Promise<TestResult[]> {
const appCheckTests = new AppCheckTests(this.firebaseConfig, this.recaptchaSiteKey);
return [
await appCheckTests.testRecaptchaInitialization(),
await appCheckTests.testTokenGeneration(),
await appCheckTests.testTokenVerification(this.baseUrl),
await appCheckTests.testImageUploadWithAppCheck(`${this.baseUrl}/api/uploads`)
];
}

async testAccessOnly(): Promise<TestResult[]> {
const accessTests = new CloudflareAccessTests(this.baseUrl);
return Promise.all([
accessTests.testAccessAuthentication(),
accessTests.testSessionManagement(),
accessTests.testPermissionValidation()
]);
}

async testSecurityOnly(): Promise<TestResult[]> {
const securityTests = new SecurityTests(this.baseUrl);
return Promise.all([
securityTests.testCORSConfiguration(),
securityTests.testSecurityHeaders(),
securityTests.testRateLimiting()
]);
}
}

// 使用例
export async function runPhase2Tests(config: {
firebaseConfig: any;
recaptchaSiteKey: string;
baseUrl: string;
}): Promise<TestSuite> {
const runner = new Phase2TestRunner(config);
return await runner.executeAll();
}

重要: このPhase 2テストにより、以前のサムネイル表示問題を防止できます。App Checkトークンの生成・検証・画像アップロード統合テストを通じて、MVP開発前に認証周りの問題を発見・解決します。

パフォーマンス目標:

  • App Checkトークン生成: <2秒
  • 認証確認: <500ms
  • 画像アップロード(App Check付き): <10秒
  • サムネイル表示確認: <3秒

関連ドキュメント: Phase 1実装 | デバッグダッシュボード