11import { vi , describe , it , expect , beforeEach } from "vitest" ;
22
3- // ── Hoisted mocks (available inside vi.mock factories) ──────────
43const { mockCollectMetrics, mockAuthenticateApiKey } = vi . hoisted ( ( ) => ( {
54 mockCollectMetrics : vi . fn ( ) ,
65 mockAuthenticateApiKey : vi . fn ( ) ,
76} ) ) ;
87
9- // ── Mock PrometheusMetricsService ───────────────────────────────
108vi . mock ( "@/server/services/prometheus-metrics" , ( ) => ( {
119 PrometheusMetricsService : class {
1210 collectMetrics = mockCollectMetrics ;
1311 } ,
1412} ) ) ;
1513
16- // ── Mock authenticateApiKey ─────────────────────────────────────
1714vi . mock ( "@/server/middleware/api-auth" , ( ) => ( {
1815 authenticateApiKey : ( ...args : unknown [ ] ) => mockAuthenticateApiKey ( ...args ) ,
16+ hasPermission : ( ctx : { permissions : string [ ] } , perm : string ) =>
17+ ctx . permissions . includes ( perm ) ,
1918} ) ) ;
2019
2120import { GET } from "@/app/api/metrics/route" ;
2221
23- // ─── Helpers ────────────────────────────────────────────────────
24-
2522function makeRequest ( headers ?: Record < string , string > ) : Request {
2623 return new Request ( "http://localhost:3000/api/metrics" , {
2724 method : "GET" ,
2825 headers : headers ?? { } ,
2926 } ) ;
3027}
3128
32- // ─── Tests ──────────────────────────────────────────────────────
33-
3429describe ( "GET /api/metrics" , ( ) => {
3530 beforeEach ( ( ) => {
3631 vi . clearAllMocks ( ) ;
37- // Default: auth not required
38- delete process . env . METRICS_AUTH_REQUIRED ;
39- } ) ;
40-
41- it ( "returns metrics with correct content type when auth disabled (default)" , async ( ) => {
42- const metricsOutput = '# HELP vectorflow_node_status Node status\nvectorflow_node_status{node_id="n1"} 1\n' ;
43- mockCollectMetrics . mockResolvedValue ( metricsOutput ) ;
44-
45- const response = await GET ( makeRequest ( ) ) ;
46-
47- expect ( response . status ) . toBe ( 200 ) ;
48- expect ( response . headers . get ( "Content-Type" ) ) . toBe (
49- "text/plain; version=0.0.4; charset=utf-8" ,
50- ) ;
51- const body = await response . text ( ) ;
52- expect ( body ) . toBe ( metricsOutput ) ;
53- expect ( mockAuthenticateApiKey ) . not . toHaveBeenCalled ( ) ;
54- } ) ;
55-
56- it ( "does not require auth header when METRICS_AUTH_REQUIRED is unset" , async ( ) => {
57- mockCollectMetrics . mockResolvedValue ( "" ) ;
58-
59- const response = await GET ( makeRequest ( ) ) ;
60- expect ( response . status ) . toBe ( 200 ) ;
61- expect ( mockAuthenticateApiKey ) . not . toHaveBeenCalled ( ) ;
6232 } ) ;
6333
64- it ( "returns 401 when auth required and no token provided" , async ( ) => {
65- process . env . METRICS_AUTH_REQUIRED = "true" ;
34+ it ( "returns 401 when no auth header provided" , async ( ) => {
6635 mockAuthenticateApiKey . mockResolvedValue ( null ) ;
6736
6837 const response = await GET ( makeRequest ( ) ) ;
6938
7039 expect ( response . status ) . toBe ( 401 ) ;
71- const body = await response . text ( ) ;
72- expect ( body ) . toContain ( "Unauthorized" ) ;
7340 expect ( mockAuthenticateApiKey ) . toHaveBeenCalledWith ( null ) ;
7441 } ) ;
7542
76- it ( "returns 401 when auth required and invalid token provided" , async ( ) => {
77- process . env . METRICS_AUTH_REQUIRED = "true" ;
43+ it ( "returns 401 when invalid token provided" , async ( ) => {
7844 mockAuthenticateApiKey . mockResolvedValue ( null ) ;
7945
8046 const response = await GET (
@@ -85,13 +51,27 @@ describe("GET /api/metrics", () => {
8551 expect ( mockAuthenticateApiKey ) . toHaveBeenCalledWith ( "Bearer invalid_token" ) ;
8652 } ) ;
8753
88- it ( "returns metrics when auth required and valid token provided" , async ( ) => {
89- process . env . METRICS_AUTH_REQUIRED = "true" ;
54+ it ( "returns 401 when token lacks metrics.read permission" , async ( ) => {
55+ mockAuthenticateApiKey . mockResolvedValue ( {
56+ serviceAccountId : "sa-1" ,
57+ serviceAccountName : "deploy-bot" ,
58+ environmentId : "env-1" ,
59+ permissions : [ "pipelines.deploy" ] ,
60+ } ) ;
61+
62+ const response = await GET (
63+ makeRequest ( { Authorization : "Bearer vf_deploy_token" } ) ,
64+ ) ;
65+
66+ expect ( response . status ) . toBe ( 401 ) ;
67+ } ) ;
68+
69+ it ( "returns metrics when valid token provided" , async ( ) => {
9070 mockAuthenticateApiKey . mockResolvedValue ( {
9171 serviceAccountId : "sa-1" ,
9272 serviceAccountName : "prom-scraper" ,
9373 environmentId : "env-1" ,
94- permissions : [ "read" ] ,
74+ permissions : [ "metrics. read" ] ,
9575 } ) ;
9676 mockCollectMetrics . mockResolvedValue ( "vectorflow_node_status 1\n" ) ;
9777
@@ -100,20 +80,28 @@ describe("GET /api/metrics", () => {
10080 ) ;
10181
10282 expect ( response . status ) . toBe ( 200 ) ;
83+ expect ( response . headers . get ( "Content-Type" ) ) . toBe (
84+ "text/plain; version=0.0.4; charset=utf-8" ,
85+ ) ;
10386 const body = await response . text ( ) ;
10487 expect ( body ) . toBe ( "vectorflow_node_status 1\n" ) ;
10588 } ) ;
10689
10790 it ( "returns 500 when collectMetrics throws" , async ( ) => {
91+ mockAuthenticateApiKey . mockResolvedValue ( {
92+ serviceAccountId : "sa-1" ,
93+ serviceAccountName : "prom-scraper" ,
94+ environmentId : "env-1" ,
95+ permissions : [ "metrics.read" ] ,
96+ } ) ;
10897 mockCollectMetrics . mockRejectedValue ( new Error ( "Service crash" ) ) ;
10998 const consoleSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
11099
111- const response = await GET ( makeRequest ( ) ) ;
100+ const response = await GET (
101+ makeRequest ( { Authorization : "Bearer vf_valid_token" } ) ,
102+ ) ;
112103
113104 expect ( response . status ) . toBe ( 500 ) ;
114- const body = await response . text ( ) ;
115- expect ( body ) . toContain ( "Internal Server Error" ) ;
116-
117105 consoleSpy . mockRestore ( ) ;
118106 } ) ;
119107} ) ;
0 commit comments