Skip to main content

🏗️ Phase 1: 基礎インフラテスト実装

📋 実装概要

MVP前の最重要テストフェーズ。Cloudflareインフラ全体の接続・動作確認。

必須成功率: 100%

すべてのインフラテストが成功する必要があります

🗄️ D1データベーステスト

接続・クエリテスト実装

// lib/test-engine/phase1/d1-tests.ts
import { TestResult } from '../types';

export class D1DatabaseTests {
private db: D1Database;

constructor(db: D1Database) {
this.db = db;
}

async testConnection(): Promise<TestResult> {
const startTime = Date.now();
const testId = `d1-connection-${Date.now()}`;

try {
console.log('🔍 D1接続テスト開始...');

// シンプル接続テスト
const result = await this.db
.prepare('SELECT 1 as test_connection, datetime("now") as current_time')
.first();

const duration = Date.now() - startTime;

if (!result) {
throw new Error('D1クエリ結果が空です');
}

console.log(`✅ D1接続成功: ${duration}ms`);

return {
id: testId,
name: 'D1データベース接続',
status: 'success',
message: `接続成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
result,
queryTime: duration,
database: 'contents-print-debug'
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ D1接続エラー:', error);

return {
id: testId,
name: 'D1データベース接続',
status: 'error',
message: `接続失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error,
details: { errorType: 'connection_failed' }
};
}
}

async testSchemaValidation(): Promise<TestResult> {
const startTime = Date.now();
const testId = `d1-schema-${Date.now()}`;

try {
console.log('🔍 D1スキーマテスト開始...');

// 必要テーブルの存在確認
const requiredTables = [
'users', 'brands', 'orders', 'images',
'test_results', 'system_logs'
];

const tableChecks = await Promise.all(
requiredTables.map(async (table) => {
const result = await this.db
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`)
.bind(table)
.first();
return { table, exists: !!result };
})
);

const missingTables = tableChecks
.filter(check => !check.exists)
.map(check => check.table);

const duration = Date.now() - startTime;

if (missingTables.length > 0) {
console.warn(`⚠️ 不足テーブル: ${missingTables.join(', ')}`);
return {
id: testId,
name: 'D1スキーマ検証',
status: 'warning',
message: `不足テーブル: ${missingTables.join(', ')}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
requiredTables,
existingTables: tableChecks.filter(c => c.exists).map(c => c.table),
missingTables
}
};
}

console.log(`✅ D1スキーマ検証完了: ${duration}ms`);

return {
id: testId,
name: 'D1スキーマ検証',
status: 'success',
message: `全テーブル確認済み (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
tables: tableChecks,
schemaVersion: 'v1.0.0'
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ D1スキーマエラー:', error);

return {
id: testId,
name: 'D1スキーマ検証',
status: 'error',
message: `スキーマ検証失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testCRUDOperations(): Promise<TestResult> {
const startTime = Date.now();
const testId = `d1-crud-${Date.now()}`;

try {
console.log('🔍 D1 CRUD操作テスト開始...');

const testData = {
id: `test-${Date.now()}`,
name: 'Debug Test User',
email: `test-${Date.now()}@debug.test`,
created_at: new Date().toISOString()
};

// CREATE - テストデータ挿入
const insertResult = await this.db
.prepare(`
INSERT INTO test_users (id, name, email, created_at)
VALUES (?, ?, ?, ?)
`)
.bind(testData.id, testData.name, testData.email, testData.created_at)
.run();

if (!insertResult.success) {
throw new Error('INSERT操作失敗');
}

// READ - データ取得確認
const selectResult = await this.db
.prepare('SELECT * FROM test_users WHERE id = ?')
.bind(testData.id)
.first();

if (!selectResult) {
throw new Error('SELECT操作でデータ取得失敗');
}

// UPDATE - データ更新
const updatedName = `${testData.name} (Updated)`;
const updateResult = await this.db
.prepare('UPDATE test_users SET name = ? WHERE id = ?')
.bind(updatedName, testData.id)
.run();

if (!updateResult.success) {
throw new Error('UPDATE操作失敗');
}

// DELETE - データ削除(クリーンアップ)
const deleteResult = await this.db
.prepare('DELETE FROM test_users WHERE id = ?')
.bind(testData.id)
.run();

if (!deleteResult.success) {
throw new Error('DELETE操作失敗');
}

const duration = Date.now() - startTime;
console.log(`✅ D1 CRUD操作完了: ${duration}ms`);

return {
id: testId,
name: 'D1 CRUD操作',
status: 'success',
message: `全CRUD操作成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
operations: ['CREATE', 'READ', 'UPDATE', 'DELETE'],
testData,
results: {
insert: insertResult,
select: selectResult,
update: updateResult,
delete: deleteResult
}
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ D1 CRUD操作エラー:', error);

return {
id: testId,
name: 'D1 CRUD操作',
status: 'error',
message: `CRUD操作失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

📦 R2ストレージテスト

アップロード・ダウンロードテスト実装

// lib/test-engine/phase1/r2-tests.ts
export class R2StorageTests {
private r2: R2Bucket;

constructor(r2: R2Bucket) {
this.r2 = r2;
}

async testConnection(): Promise<TestResult> {
const startTime = Date.now();
const testId = `r2-connection-${Date.now()}`;

try {
console.log('🔍 R2接続テスト開始...');

// バケット情報取得テスト
const testKey = `debug/connection-test-${Date.now()}.txt`;
const testContent = 'R2 Connection Test';

const uploadResult = await this.r2.put(testKey, testContent);

if (!uploadResult) {
throw new Error('R2アップロード失敗');
}

// アップロードしたファイルを削除(クリーンアップ)
await this.r2.delete(testKey);

const duration = Date.now() - startTime;
console.log(`✅ R2接続成功: ${duration}ms`);

return {
id: testId,
name: 'R2ストレージ接続',
status: 'success',
message: `接続成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
bucket: 'contents-print-storage',
testKey,
contentLength: testContent.length
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ R2接続エラー:', error);

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

async testImageUpload(): Promise<TestResult> {
const startTime = Date.now();
const testId = `r2-image-upload-${Date.now()}`;

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

// テスト用画像データ生成(1x1ピクセルPNG)
const testImageData = new Uint8Array([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82
]);

const testKey = `debug/test-image-${Date.now()}.png`;

// 画像アップロード
const uploadResult = await this.r2.put(testKey, testImageData, {
httpMetadata: {
contentType: 'image/png'
}
});

if (!uploadResult) {
throw new Error('画像アップロード失敗');
}

// アップロード確認
const getResult = await this.r2.get(testKey);

if (!getResult) {
throw new Error('アップロードした画像が見つからない');
}

const downloadedData = new Uint8Array(await getResult.arrayBuffer());

if (downloadedData.length !== testImageData.length) {
throw new Error('ダウンロードデータサイズが不一致');
}

// クリーンアップ
await this.r2.delete(testKey);

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

return {
id: testId,
name: 'R2画像アップロード',
status: 'success',
message: `画像アップロード成功 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
testKey,
originalSize: testImageData.length,
downloadedSize: downloadedData.length,
contentType: 'image/png',
uploadTime: duration
}
};

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

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

async testPerformanceBenchmark(): Promise<TestResult> {
const startTime = Date.now();
const testId = `r2-performance-${Date.now()}`;

try {
console.log('🔍 R2パフォーマンステスト開始...');

const testSizes = [
{ name: 'Small', size: 1024 }, // 1KB
{ name: 'Medium', size: 1024 * 100 }, // 100KB
{ name: 'Large', size: 1024 * 1024 } // 1MB
];

const results = [];

for (const test of testSizes) {
const testData = new Uint8Array(test.size).fill(65); // 'A'で埋める
const testKey = `debug/perf-${test.name.toLowerCase()}-${Date.now()}`;

const uploadStart = performance.now();
await this.r2.put(testKey, testData);
const uploadTime = performance.now() - uploadStart;

const downloadStart = performance.now();
const downloaded = await this.r2.get(testKey);
await downloaded?.arrayBuffer();
const downloadTime = performance.now() - downloadStart;

// クリーンアップ
await this.r2.delete(testKey);

results.push({
size: test.name,
bytes: test.size,
uploadTime: Math.round(uploadTime),
downloadTime: Math.round(downloadTime),
uploadThroughput: Math.round(test.size / uploadTime * 1000), // bytes/sec
downloadThroughput: Math.round(test.size / downloadTime * 1000)
});
}

const duration = Date.now() - startTime;
console.log(`✅ R2パフォーマンステスト完了: ${duration}ms`);

// パフォーマンス基準チェック
const slowResults = results.filter(r => r.uploadTime > 5000 || r.downloadTime > 5000);
const status = slowResults.length > 0 ? 'warning' : 'success';
const message = slowResults.length > 0
? `パフォーマンス警告: 遅い操作 ${slowResults.length}`
: `パフォーマンス良好 (${duration}ms)`;

return {
id: testId,
name: 'R2パフォーマンステスト',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
results,
slowOperations: slowResults,
averageUploadTime: results.reduce((sum, r) => sum + r.uploadTime, 0) / results.length,
averageDownloadTime: results.reduce((sum, r) => sum + r.downloadTime, 0) / results.length
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ R2パフォーマンステストエラー:', error);

return {
id: testId,
name: 'R2パフォーマンステスト',
status: 'error',
message: `パフォーマンステスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

⚡ Cloudflare Workersテスト

API・ルーティングテスト実装

// lib/test-engine/phase1/workers-tests.ts
export class WorkersAPITests {
private baseUrl: string;

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

async testDeploymentStatus(): Promise<TestResult> {
const startTime = Date.now();
const testId = `workers-deployment-${Date.now()}`;

try {
console.log('🔍 Workers展開状態テスト開始...');

const response = await fetch(`${this.baseUrl}/api/health`, {
method: 'GET',
headers: {
'User-Agent': 'Debug-Test-Suite/1.0'
}
});

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

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

console.log(`✅ Workers展開確認: ${duration}ms`);

return {
id: testId,
name: 'Workers展開状態',
status: 'success',
message: `展開正常 (${duration}ms)`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
healthCheck: healthData,
responseTime: duration,
status: response.status,
headers: Object.fromEntries(response.headers.entries())
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Workers展開状態エラー:', error);

return {
id: testId,
name: 'Workers展開状態',
status: 'error',
message: `展開確認失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testAPIRouting(): Promise<TestResult> {
const startTime = Date.now();
const testId = `workers-routing-${Date.now()}`;

try {
console.log('🔍 Workers APIルーティングテスト開始...');

const testRoutes = [
{ path: '/api/health', expectedStatus: 200 },
{ path: '/api/brands', expectedStatus: 200 },
{ path: '/api/debug/status', expectedStatus: 200 },
{ path: '/api/nonexistent', expectedStatus: 404 }
];

const routeResults = [];

for (const route of testRoutes) {
const routeStart = performance.now();

try {
const response = await fetch(`${this.baseUrl}${route.path}`);
const routeTime = Math.round(performance.now() - routeStart);

routeResults.push({
path: route.path,
expectedStatus: route.expectedStatus,
actualStatus: response.status,
responseTime: routeTime,
success: response.status === route.expectedStatus
});

} catch (routeError) {
routeResults.push({
path: route.path,
expectedStatus: route.expectedStatus,
actualStatus: null,
responseTime: Math.round(performance.now() - routeStart),
success: false,
error: (routeError as Error).message
});
}
}

const failedRoutes = routeResults.filter(r => !r.success);
const duration = Date.now() - startTime;

console.log(`✅ Workers APIルーティング完了: ${duration}ms`);

return {
id: testId,
name: 'Workers APIルーティング',
status: failedRoutes.length === 0 ? 'success' : 'error',
message: failedRoutes.length === 0
? `全ルート正常 (${duration}ms)`
: `失敗ルート ${failedRoutes.length}/${routeResults.length}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
routeResults,
failedRoutes,
successRate: ((routeResults.length - failedRoutes.length) / routeResults.length * 100).toFixed(1)
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Workers APIルーティングエラー:', error);

return {
id: testId,
name: 'Workers APIルーティング',
status: 'error',
message: `ルーティングテスト失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}

async testEnvironmentVariables(): Promise<TestResult> {
const startTime = Date.now();
const testId = `workers-env-${Date.now()}`;

try {
console.log('🔍 Workers環境変数テスト開始...');

const response = await fetch(`${this.baseUrl}/api/debug/env`, {
method: 'GET',
headers: {
'Authorization': 'Bearer debug-test-token'
}
});

if (!response.ok) {
throw new Error(`環境変数API: HTTP ${response.status}`);
}

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

// 必須環境変数のチェック
const requiredVars = [
'ENVIRONMENT',
'DATABASE_ID',
'STORAGE_BUCKET',
'FIREBASE_PROJECT_ID'
];

const missingVars = requiredVars.filter(
varName => !envData.variables[varName]
);

const status = missingVars.length === 0 ? 'success' : 'warning';
const message = missingVars.length === 0
? `環境変数設定完了 (${duration}ms)`
: `不足環境変数: ${missingVars.join(', ')}`;

console.log(`✅ Workers環境変数確認: ${duration}ms`);

return {
id: testId,
name: 'Workers環境変数',
status,
message,
startTime: new Date(startTime),
endTime: new Date(),
duration,
details: {
availableVars: Object.keys(envData.variables),
requiredVars,
missingVars,
environment: envData.variables.ENVIRONMENT || 'unknown'
}
};

} catch (error) {
const duration = Date.now() - startTime;
console.error('❌ Workers環境変数エラー:', error);

return {
id: testId,
name: 'Workers環境変数',
status: 'error',
message: `環境変数確認失敗: ${error.message}`,
startTime: new Date(startTime),
endTime: new Date(),
duration,
error: error as Error
};
}
}
}

📊 Phase 1 統合実行エンジン

テスト実行・結果集約

// lib/test-engine/phase1/index.ts
import { D1DatabaseTests } from './d1-tests';
import { R2StorageTests } from './r2-tests';
import { WorkersAPITests } from './workers-tests';
import { TestResult, TestSuite } from '../types';

export class Phase1TestRunner {
private d1: D1Database;
private r2: R2Bucket;
private workersUrl: string;

constructor(bindings: {
DB: D1Database;
STORAGE: R2Bucket;
WORKERS_URL: string
}) {
this.d1 = bindings.DB;
this.r2 = bindings.STORAGE;
this.workersUrl = bindings.WORKERS_URL;
}

async executeAll(): Promise<TestSuite> {
const startTime = new Date();
console.log('🚀 Phase 1: 基礎インフラテスト開始');

const d1Tests = new D1DatabaseTests(this.d1);
const r2Tests = new R2StorageTests(this.r2);
const workersTests = new WorkersAPITests(this.workersUrl);

const allTests: TestResult[] = [];

try {
// D1データベーステスト実行
console.log('📊 D1データベーステスト...');
const d1Results = await Promise.all([
d1Tests.testConnection(),
d1Tests.testSchemaValidation(),
d1Tests.testCRUDOperations()
]);
allTests.push(...d1Results);

// R2ストレージテスト実行
console.log('📦 R2ストレージテスト...');
const r2Results = await Promise.all([
r2Tests.testConnection(),
r2Tests.testImageUpload(),
r2Tests.testPerformanceBenchmark()
]);
allTests.push(...r2Results);

// Workersテスト実行
console.log('⚡ Workersテスト...');
const workersResults = await Promise.all([
workersTests.testDeploymentStatus(),
workersTests.testAPIRouting(),
workersTests.testEnvironmentVariables()
]);
allTests.push(...workersResults);

} catch (error) {
console.error('❌ Phase 1テスト実行エラー:', 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: `phase1-${Date.now()}`,
name: 'Phase 1: 基礎インフラストラクチャ',
phase: 1,
priority: 'critical',
requiredSuccessRate: 100,
tests: allTests,
overallStatus,
startTime,
endTime,
successRate: Math.round(successRate)
};

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

if (successRate < 100) {
console.warn(`⚠️ Phase 1: 必須成功率100%未満 (${successRate.toFixed(1)}%)`);
console.warn('MVP開発前にすべてのインフラ問題を解決してください');
}

return suite;
}

// 個別テスト実行メソッド
async testD1Only(): Promise<TestResult[]> {
const d1Tests = new D1DatabaseTests(this.d1);
return Promise.all([
d1Tests.testConnection(),
d1Tests.testSchemaValidation(),
d1Tests.testCRUDOperations()
]);
}

async testR2Only(): Promise<TestResult[]> {
const r2Tests = new R2StorageTests(this.r2);
return Promise.all([
r2Tests.testConnection(),
r2Tests.testImageUpload(),
r2Tests.testPerformanceBenchmark()
]);
}

async testWorkersOnly(): Promise<TestResult[]> {
const workersTests = new WorkersAPITests(this.workersUrl);
return Promise.all([
workersTests.testDeploymentStatus(),
workersTests.testAPIRouting(),
workersTests.testEnvironmentVariables()
]);
}
}

// 使用例
export async function runPhase1Tests(env: Env): Promise<TestSuite> {
const runner = new Phase1TestRunner({
DB: env.DB,
STORAGE: env.STORAGE,
WORKERS_URL: 'https://api.contents-print.jp'
});

return await runner.executeAll();
}

📈 結果保存・レポート

テスト結果永続化

// lib/test-engine/phase1/results-storage.ts
export class TestResultsStorage {
private db: D1Database;

constructor(db: D1Database) {
this.db = db;
}

async saveTestSuite(suite: TestSuite): Promise<void> {
try {
// テストスイート保存
await this.db.prepare(`
INSERT INTO test_suites (
id, name, phase, priority, required_success_rate,
overall_status, start_time, end_time, success_rate,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
suite.id,
suite.name,
suite.phase,
suite.priority,
suite.requiredSuccessRate,
suite.overallStatus,
suite.startTime?.toISOString(),
suite.endTime?.toISOString(),
suite.successRate,
new Date().toISOString()
).run();

// 個別テスト結果保存
for (const test of suite.tests) {
await this.db.prepare(`
INSERT INTO test_results (
id, suite_id, name, status, message,
start_time, end_time, duration, details, error_message,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
test.id,
suite.id,
test.name,
test.status,
test.message,
test.startTime.toISOString(),
test.endTime?.toISOString(),
test.duration,
JSON.stringify(test.details || {}),
test.error?.message || null,
new Date().toISOString()
).run();
}

} catch (error) {
console.error('テスト結果保存エラー:', error);
throw error;
}
}

async getLatestResults(phase: number, limit = 10): Promise<TestSuite[]> {
try {
const suites = await this.db.prepare(`
SELECT * FROM test_suites
WHERE phase = ?
ORDER BY created_at DESC
LIMIT ?
`).bind(phase, limit).all();

const results: TestSuite[] = [];

for (const suite of suites.results) {
const tests = await this.db.prepare(`
SELECT * FROM test_results
WHERE suite_id = ?
ORDER BY created_at ASC
`).bind(suite.id).all();

results.push({
...suite,
tests: tests.results.map(test => ({
...test,
startTime: new Date(test.start_time),
endTime: test.end_time ? new Date(test.end_time) : undefined,
details: test.details ? JSON.parse(test.details) : undefined,
error: test.error_message ? new Error(test.error_message) : undefined
}))
} as TestSuite);
}

return results;

} catch (error) {
console.error('テスト結果取得エラー:', error);
throw error;
}
}
}

次のステップ: Phase 2認証・セキュリティテスト実装

パフォーマンス目標:

  • D1接続: <100ms
  • R2アップロード: <5秒
  • Workers API: <200ms
  • 全体成功率: 100%

関連ドキュメント: デバッグダッシュボード | 型定義