Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1fe3713a |
@@ -1,36 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
import { calculateAP } from './useFMEA'
|
|
||||||
|
|
||||||
describe('calculateAP — AIAG-VDA 2019 Handbook Action Priority', () => {
|
|
||||||
it('returns H for severity 10 with mid occurrence', () => {
|
|
||||||
expect(calculateAP(10, 5, 5)).toBe('H')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns H for severity 9 with low detection', () => {
|
|
||||||
expect(calculateAP(9, 4, 7)).toBe('H')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns M for severity 9 with low occurrence and good detection', () => {
|
|
||||||
expect(calculateAP(9, 2, 5)).toBe('M')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns L for severity 9 with very low occurrence and detection', () => {
|
|
||||||
expect(calculateAP(9, 1, 4)).toBe('L')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns H for severity 7 with high occurrence', () => {
|
|
||||||
expect(calculateAP(7, 5, 1)).toBe('H')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns M for severity 7 with mid occurrence', () => {
|
|
||||||
expect(calculateAP(7, 3, 5)).toBe('M')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns L for low-severity well-controlled mode', () => {
|
|
||||||
expect(calculateAP(3, 1, 1)).toBe('L')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns L for severity 5 with very low occurrence and detection', () => {
|
|
||||||
expect(calculateAP(5, 1, 1)).toBe('L')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -156,52 +156,5 @@ export function useFMEA(projectId: string) {
|
|||||||
// Get unique components for the suggest button
|
// Get unique components for the suggest button
|
||||||
const components = [...new Map(rows.map((r) => [r.component.id, r.component])).values()]
|
const components = [...new Map(rows.map((r) => [r.component.id, r.component])).values()]
|
||||||
|
|
||||||
/**
|
return { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions }
|
||||||
* Accept a suggested FM: build an FMEA row from the FM defaults, prepend it
|
|
||||||
* to the table state, and remove the FM from the suggestion list.
|
|
||||||
* Returns false if the (component, fm.id) combo already exists in rows.
|
|
||||||
*/
|
|
||||||
function acceptSuggestion(fm: FailureMode, componentId: string): boolean {
|
|
||||||
const comp = components.find((c) => c.id === componentId)
|
|
||||||
if (!comp) return false
|
|
||||||
const dup = rows.find((r) => r.component.id === componentId && r.failureMode.id === fm.id)
|
|
||||||
if (dup) {
|
|
||||||
// Still drop the suggestion so the UI does not keep offering it.
|
|
||||||
setSuggestions((prev) => prev.filter((s) => s.id !== fm.id))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const s = fm.default_severity || 5
|
|
||||||
const o = fm.default_occurrence || 5
|
|
||||||
const d = fm.default_detection || 5
|
|
||||||
const newRow: FMEARow = {
|
|
||||||
component: comp,
|
|
||||||
failureMode: fm,
|
|
||||||
severity: s,
|
|
||||||
occurrence: o,
|
|
||||||
detection: d,
|
|
||||||
rpz: s * o * d,
|
|
||||||
ap: calculateAP(s, o, d),
|
|
||||||
}
|
|
||||||
setRows((prev) => [newRow, ...prev].sort((a, b) => b.rpz - a.rpz))
|
|
||||||
setSuggestions((prev) => prev.filter((sg) => sg.id !== fm.id))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function rejectSuggestion(fmId: string) {
|
|
||||||
setSuggestions((prev) => prev.filter((sg) => sg.id !== fmId))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows,
|
|
||||||
loading,
|
|
||||||
stats,
|
|
||||||
components,
|
|
||||||
suggestFMs,
|
|
||||||
suggesting,
|
|
||||||
suggestions,
|
|
||||||
suggestSource,
|
|
||||||
setSuggestions,
|
|
||||||
acceptSuggestion,
|
|
||||||
rejectSuggestion,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useFMEA, type FMEARow } from './_hooks/useFMEA'
|
import { useFMEA, type FMEARow } from './_hooks/useFMEA'
|
||||||
|
|
||||||
@@ -27,17 +27,8 @@ function rpzLabel(rpz: number): string {
|
|||||||
|
|
||||||
export default function FMEAPage() {
|
export default function FMEAPage() {
|
||||||
const { projectId } = useParams<{ projectId: string }>()
|
const { projectId } = useParams<{ projectId: string }>()
|
||||||
const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions, acceptSuggestion, rejectSuggestion } = useFMEA(projectId)
|
const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions } = useFMEA(projectId)
|
||||||
const [suggestComp, setSuggestComp] = useState<string | null>(null)
|
const [suggestComp, setSuggestComp] = useState<string | null>(null)
|
||||||
const [acceptedCount, setAcceptedCount] = useState(0)
|
|
||||||
|
|
||||||
// Reset accepted-count when a fresh suggestion run is loaded or the panel closes.
|
|
||||||
useEffect(() => {
|
|
||||||
if (suggesting) setAcceptedCount(0)
|
|
||||||
}, [suggesting])
|
|
||||||
useEffect(() => {
|
|
||||||
if (suggestions.length === 0) setAcceptedCount(0)
|
|
||||||
}, [suggestions.length])
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -106,23 +97,14 @@ export default function FMEAPage() {
|
|||||||
{suggestions.length > 0 && (
|
{suggestions.length > 0 && (
|
||||||
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-xl p-4">
|
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-xl p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-purple-800 dark:text-purple-300">
|
<h3 className="text-sm font-semibold text-purple-800 dark:text-purple-300">
|
||||||
KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek-Fallback'}
|
KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek'}
|
||||||
</h3>
|
</h3>
|
||||||
{acceptedCount > 0 && (
|
|
||||||
<div className="text-xs text-green-700 dark:text-green-400 mt-0.5">
|
|
||||||
{acceptedCount} Vorschlag{acceptedCount > 1 ? 'e' : ''} uebernommen
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button onClick={() => setSuggestions([])} className="text-xs text-purple-600 hover:text-purple-800">Schliessen</button>
|
<button onClick={() => setSuggestions([])} className="text-xs text-purple-600 hover:text-purple-800">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{suggestions.map((fm) => {
|
{suggestions.map((fm, i) => (
|
||||||
const rpz = fm.default_severity * fm.default_occurrence * fm.default_detection
|
<div key={i} className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3 border border-purple-100 dark:border-purple-800">
|
||||||
return (
|
|
||||||
<div key={fm.id} className="flex items-start justify-between gap-3 bg-white dark:bg-gray-800 rounded-lg p-3 border border-purple-100 dark:border-purple-800">
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{fm.name_de}</div>
|
<div className="text-sm font-medium text-gray-900 dark:text-white">{fm.name_de}</div>
|
||||||
<div className="text-xs text-gray-500 mt-0.5">{fm.effect}</div>
|
<div className="text-xs text-gray-500 mt-0.5">{fm.effect}</div>
|
||||||
@@ -130,36 +112,11 @@ export default function FMEAPage() {
|
|||||||
<span>S={fm.default_severity}</span>
|
<span>S={fm.default_severity}</span>
|
||||||
<span>O={fm.default_occurrence}</span>
|
<span>O={fm.default_occurrence}</span>
|
||||||
<span>D={fm.default_detection}</span>
|
<span>D={fm.default_detection}</span>
|
||||||
<span className={`font-bold ${rpz > 200 ? 'text-red-600' : rpz > 100 ? 'text-orange-600' : 'text-gray-500'}`}>RPZ={rpz}</span>
|
<span className="font-bold">RPZ={fm.default_severity * fm.default_occurrence * fm.default_detection}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!suggestComp) return
|
|
||||||
const ok = acceptSuggestion(fm, suggestComp)
|
|
||||||
if (ok) setAcceptedCount((c) => c + 1)
|
|
||||||
}}
|
|
||||||
disabled={!suggestComp}
|
|
||||||
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white text-xs font-medium rounded transition-colors"
|
|
||||||
title="Diesen Fehlermodus der FMEA-Tabelle hinzufuegen"
|
|
||||||
>
|
|
||||||
Uebernehmen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => rejectSuggestion(fm.id)}
|
|
||||||
className="px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-xs font-medium rounded transition-colors"
|
|
||||||
title="Diesen Vorschlag verwerfen"
|
|
||||||
>
|
|
||||||
Ablehnen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] text-purple-700 dark:text-purple-400 mt-3">
|
|
||||||
Hinweis: Uebernommene Fehlermodi erscheinen sofort in der Tabelle unten. Bewertung (S/O/D) ist anpassbar — Standardwerte aus der Bibliothek.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
|
|||||||
<AdditionalModuleItem href="/sdk/ai-registration" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>} label="EU Registrierung" isActive={pathname?.startsWith('/sdk/ai-registration') ?? false} collapsed={collapsed} projectId={projectId} />
|
<AdditionalModuleItem href="/sdk/ai-registration" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>} label="EU Registrierung" isActive={pathname?.startsWith('/sdk/ai-registration') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||||
<AdditionalModuleItem href="/sdk/compliance-optimizer" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>} label="Compliance Optimizer" isActive={pathname?.startsWith('/sdk/compliance-optimizer') ?? false} collapsed={collapsed} projectId={projectId} />
|
<AdditionalModuleItem href="/sdk/compliance-optimizer" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>} label="Compliance Optimizer" isActive={pathname?.startsWith('/sdk/compliance-optimizer') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||||
<AdditionalModuleItem href="/sdk/agent" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>} label="Compliance Agent" isActive={pathname?.startsWith('/sdk/agent') ?? false} collapsed={collapsed} projectId={projectId} />
|
<AdditionalModuleItem href="/sdk/agent" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>} label="Compliance Agent" isActive={pathname?.startsWith('/sdk/agent') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||||
<AdditionalModuleItem href="/sdk/benchmark" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>} label="Branchen-Benchmark" isActive={pathname?.startsWith('/sdk/benchmark') ?? false} collapsed={collapsed} projectId={projectId} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CRA Compliance */}
|
{/* CRA Compliance */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -412,7 +413,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("PDF export failed: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("PDF export failed: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.pdf", safeName), projectID.String())
|
h.archiveTechFile(c, data, fmt.Sprintf("CE-Akte-%s.pdf", safeName), projectID)
|
||||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.pdf"`, safeName))
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.pdf"`, safeName))
|
||||||
c.Data(http.StatusOK, "application/pdf", data)
|
c.Data(http.StatusOK, "application/pdf", data)
|
||||||
|
|
||||||
@@ -422,7 +423,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel export failed: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel export failed: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.xlsx", safeName), projectID.String())
|
h.archiveTechFile(c, data, fmt.Sprintf("CE-Akte-%s.xlsx", safeName), projectID)
|
||||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.xlsx"`, safeName))
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.xlsx"`, safeName))
|
||||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
||||||
|
|
||||||
@@ -432,7 +433,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("DOCX export failed: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("DOCX export failed: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.docx", safeName), projectID.String())
|
h.archiveTechFile(c, data, fmt.Sprintf("CE-Akte-%s.docx", safeName), projectID)
|
||||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.docx"`, safeName))
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.docx"`, safeName))
|
||||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data)
|
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data)
|
||||||
|
|
||||||
@@ -442,7 +443,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Markdown export failed: %v", err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Markdown export failed: %v", err)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.md", safeName), projectID.String())
|
h.archiveTechFile(c, data, fmt.Sprintf("CE-Akte-%s.md", safeName), projectID)
|
||||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.md"`, safeName))
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.md"`, safeName))
|
||||||
c.Data(http.StatusOK, "text/markdown", data)
|
c.Data(http.StatusOK, "text/markdown", data)
|
||||||
|
|
||||||
@@ -468,7 +469,30 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// archiveTechFile stores a tech-file export to DSMS (best-effort, non-blocking).
|
// archiveTechFile stores a tech-file export to DSMS (best-effort, non-blocking)
|
||||||
func archiveTechFile(data []byte, filename, projectID string) {
|
// AND records the resulting CID in the IACE audit trail so the export is
|
||||||
dsms.Archive(data, filename, "ce_techfile", projectID, "1")
|
// traceable. The "new_values" JSON carries the CID + filename so the audit
|
||||||
|
// timeline can later resolve the CID against the DSMS gateway for verify.
|
||||||
|
func (h *IACEHandler) archiveTechFile(c *gin.Context, data []byte, filename string, projectID uuid.UUID) {
|
||||||
|
result := dsms.Archive(data, filename, "ce_techfile", projectID.String(), "1")
|
||||||
|
if result == nil || result.CID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := map[string]string{
|
||||||
|
"cid": result.CID,
|
||||||
|
"filename": filename,
|
||||||
|
"size": fmt.Sprintf("%d", result.Size),
|
||||||
|
}
|
||||||
|
newValues, _ := json.Marshal(payload)
|
||||||
|
userID := rbac.GetUserID(c)
|
||||||
|
_ = h.store.AddAuditEntry(
|
||||||
|
c.Request.Context(),
|
||||||
|
projectID,
|
||||||
|
"tech_file_export",
|
||||||
|
projectID,
|
||||||
|
iace.AuditActionCreate,
|
||||||
|
userID.String(),
|
||||||
|
nil,
|
||||||
|
newValues,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package dsms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArchive_Success_ReturnsCID(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" || r.URL.Path != "/api/v1/documents" {
|
||||||
|
http.Error(w, "wrong route", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") {
|
||||||
|
http.Error(w, "wrong content-type", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Header.Get("Authorization") == "" {
|
||||||
|
http.Error(w, "missing auth", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
io.ReadAll(r.Body)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(ArchiveResult{
|
||||||
|
CID: "bafytest123",
|
||||||
|
Size: 42,
|
||||||
|
GatewayURL: "/ipfs/bafytest123",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
old := gatewayURL
|
||||||
|
defer func() { gatewayURL = old }()
|
||||||
|
gatewayURL = server.URL
|
||||||
|
|
||||||
|
got := Archive([]byte("hello"), "test.pdf", "ce_techfile", "proj-1", "1")
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("expected non-nil result on 200 OK")
|
||||||
|
}
|
||||||
|
if got.CID != "bafytest123" {
|
||||||
|
t.Errorf("expected CID bafytest123, got %q", got.CID)
|
||||||
|
}
|
||||||
|
if got.Size != 42 {
|
||||||
|
t.Errorf("expected Size 42, got %d", got.Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArchive_GatewayDown_ReturnsNil(t *testing.T) {
|
||||||
|
old := gatewayURL
|
||||||
|
defer func() { gatewayURL = old }()
|
||||||
|
gatewayURL = "http://127.0.0.1:1" // unreachable
|
||||||
|
got := Archive([]byte("hello"), "test.pdf", "ce_techfile", "proj-1", "1")
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("expected nil when gateway unreachable, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArchive_GatewayReturnsError_ReturnsNil(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
old := gatewayURL
|
||||||
|
defer func() { gatewayURL = old }()
|
||||||
|
gatewayURL = server.URL
|
||||||
|
|
||||||
|
got := Archive([]byte("hello"), "test.pdf", "ce_techfile", "proj-1", "1")
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("expected nil on 500 response, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user