All files / web/src/app/api/smoke-test-results route.ts

0% Statements 0/155
0% Branches 0/1
0% Functions 0/1
0% Lines 0/155

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                                                                                                                                                                                                                                                                                       
/**
 * Smoke test results endpoint
 *
 * POST /api/smoke-test-results
 *
 * Receives test results from the smoke test CronJob and stores them in the database.
 * Protected by internal cluster networking (no auth required - only accessible from within the cluster).
 *
 * Request body:
 * {
 *   id: string,          // Unique run ID
 *   startedAt: string,   // ISO timestamp
 *   completedAt?: string,// ISO timestamp (optional if still running)
 *   status: 'running' | 'passed' | 'failed' | 'error',
 *   totalTests?: number,
 *   passedTests?: number,
 *   failedTests?: number,
 *   durationMs?: number,
 *   resultsJson?: string, // JSON-stringified detailed results
 *   errorMessage?: string,
 * }
 */

import { type NextRequest, NextResponse } from 'next/server'
import { desc, inArray } from 'drizzle-orm'
import { db } from '@/db'
import { smokeTestRuns } from '@/db/schema'
import { metrics, updateSmokeTestMetrics } from '@/lib/metrics'
import { withAuth } from '@/lib/auth/withAuth'

export const dynamic = 'force-dynamic'

interface SmokeTestResultsRequest {
  id: string
  startedAt: string
  completedAt?: string
  status: 'running' | 'passed' | 'failed' | 'error'
  totalTests?: number
  passedTests?: number
  failedTests?: number
  durationMs?: number
  resultsJson?: string
  errorMessage?: string
}

interface SmokeTestResultsResponse {
  success: boolean
  id: string
  message?: string
}

export const POST = withAuth(async (request): Promise<NextResponse<SmokeTestResultsResponse>> => {
  try {
    const body = (await request.json()) as SmokeTestResultsRequest

    // Validate required fields
    if (!body.id || !body.startedAt || !body.status) {
      return NextResponse.json(
        {
          success: false,
          id: '',
          message: 'Missing required fields: id, startedAt, status',
        },
        { status: 400 }
      )
    }

    // Validate status
    if (!['running', 'passed', 'failed', 'error'].includes(body.status)) {
      return NextResponse.json(
        { success: false, id: '', message: 'Invalid status value' },
        { status: 400 }
      )
    }

    // Insert or update the test run
    await db
      .insert(smokeTestRuns)
      .values({
        id: body.id,
        startedAt: new Date(body.startedAt),
        completedAt: body.completedAt ? new Date(body.completedAt) : null,
        status: body.status,
        totalTests: body.totalTests ?? null,
        passedTests: body.passedTests ?? null,
        failedTests: body.failedTests ?? null,
        durationMs: body.durationMs ?? null,
        resultsJson: body.resultsJson ?? null,
        errorMessage: body.errorMessage ?? null,
      })
      .onConflictDoUpdate({
        target: smokeTestRuns.id,
        set: {
          completedAt: body.completedAt ? new Date(body.completedAt) : null,
          status: body.status,
          totalTests: body.totalTests ?? null,
          passedTests: body.passedTests ?? null,
          failedTests: body.failedTests ?? null,
          durationMs: body.durationMs ?? null,
          resultsJson: body.resultsJson ?? null,
          errorMessage: body.errorMessage ?? null,
        },
      })

    // Update Prometheus metrics for completed runs
    if (body.status !== 'running') {
      updateSmokeTestMetrics({
        status: body.status,
        startedAt: new Date(body.startedAt),
        completedAt: body.completedAt ? new Date(body.completedAt) : null,
        totalTests: body.totalTests ?? null,
        passedTests: body.passedTests ?? null,
        failedTests: body.failedTests ?? null,
        durationMs: body.durationMs ?? null,
      })
      metrics.smokeTest.runsTotal.inc({ status: body.status })
    }

    // Clean up old test runs (keep last 100)
    // Get IDs to keep (newest 100)
    const runsToKeep = await db
      .select({ id: smokeTestRuns.id })
      .from(smokeTestRuns)
      .orderBy(desc(smokeTestRuns.startedAt))
      .limit(100)

    if (runsToKeep.length >= 100) {
      // Get all run IDs
      const allRuns = await db.select({ id: smokeTestRuns.id }).from(smokeTestRuns)

      const keepIds = new Set(runsToKeep.map((r) => r.id))
      const idsToDelete = allRuns.filter((r) => !keepIds.has(r.id)).map((r) => r.id)

      if (idsToDelete.length > 0) {
        await db.delete(smokeTestRuns).where(inArray(smokeTestRuns.id, idsToDelete))
      }
    }

    return NextResponse.json({
      success: true,
      id: body.id,
      message: 'Test results recorded',
    })
  } catch (error) {
    console.error('Error storing smoke test results:', error)
    return NextResponse.json(
      {
        success: false,
        id: '',
        message: error instanceof Error ? error.message : 'Unknown error',
      },
      { status: 500 }
    )
  }
})