@@ -2020,6 +2020,251 @@ describe("API", () => {
20202020 } ) ;
20212021 } ) ;
20222022
2023+ // Deployments + query routes (TRI-8739). Read-only family with
2024+ // distinct resource types per route:
2025+ // - GET /api/v1/deployments { type: "deployments", id: "list" }
2026+ // - GET /api/v1/query/schema { type: "query", id: "schema" }
2027+ // - GET /api/v1/query/dashboards { type: "query", id: "dashboards" }
2028+ // - POST /api/v1/query body-derived: detectTables(query) →
2029+ // [{ type: "query", id }] or
2030+ // { type: "query", id: "all" } if none
2031+ describe ( "Deployments list — GET /api/v1/deployments" , ( ) => {
2032+ const path = "/api/v1/deployments" ;
2033+ const get = ( headers : Record < string , string > ) =>
2034+ getTestServer ( ) . webapp . fetch ( path , { headers } ) ;
2035+
2036+ it ( "missing auth: 401" , async ( ) => {
2037+ const res = await getTestServer ( ) . webapp . fetch ( path ) ;
2038+ expect ( res . status ) . toBe ( 401 ) ;
2039+ } ) ;
2040+
2041+ it ( "private API key: auth passes" , async ( ) => {
2042+ const server = getTestServer ( ) ;
2043+ const seed = await seedTestEnvironment ( server . prisma ) ;
2044+ const res = await get ( { Authorization : `Bearer ${ seed . apiKey } ` } ) ;
2045+ expect ( res . status ) . not . toBe ( 401 ) ;
2046+ expect ( res . status ) . not . toBe ( 403 ) ;
2047+ } ) ;
2048+
2049+ it ( "JWT read:deployments: auth passes" , async ( ) => {
2050+ const server = getTestServer ( ) ;
2051+ const seed = await seedTestEnvironment ( server . prisma ) ;
2052+ const jwt = await generateJWT ( {
2053+ secretKey : seed . apiKey ,
2054+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:deployments" ] } ,
2055+ expirationTime : "15m" ,
2056+ } ) ;
2057+ const res = await get ( { Authorization : `Bearer ${ jwt } ` } ) ;
2058+ expect ( res . status ) . not . toBe ( 401 ) ;
2059+ expect ( res . status ) . not . toBe ( 403 ) ;
2060+ } ) ;
2061+
2062+ it ( "JWT read:all: auth passes" , async ( ) => {
2063+ const server = getTestServer ( ) ;
2064+ const seed = await seedTestEnvironment ( server . prisma ) ;
2065+ const jwt = await generateJWT ( {
2066+ secretKey : seed . apiKey ,
2067+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:all" ] } ,
2068+ expirationTime : "15m" ,
2069+ } ) ;
2070+ const res = await get ( { Authorization : `Bearer ${ jwt } ` } ) ;
2071+ expect ( res . status ) . not . toBe ( 401 ) ;
2072+ expect ( res . status ) . not . toBe ( 403 ) ;
2073+ } ) ;
2074+
2075+ it ( "JWT admin: auth passes" , async ( ) => {
2076+ const server = getTestServer ( ) ;
2077+ const seed = await seedTestEnvironment ( server . prisma ) ;
2078+ const jwt = await generateJWT ( {
2079+ secretKey : seed . apiKey ,
2080+ payload : { pub : true , sub : seed . environment . id , scopes : [ "admin" ] } ,
2081+ expirationTime : "15m" ,
2082+ } ) ;
2083+ const res = await get ( { Authorization : `Bearer ${ jwt } ` } ) ;
2084+ expect ( res . status ) . not . toBe ( 401 ) ;
2085+ expect ( res . status ) . not . toBe ( 403 ) ;
2086+ } ) ;
2087+
2088+ it ( "JWT read:runs (type mismatch): 403" , async ( ) => {
2089+ const server = getTestServer ( ) ;
2090+ const seed = await seedTestEnvironment ( server . prisma ) ;
2091+ const jwt = await generateJWT ( {
2092+ secretKey : seed . apiKey ,
2093+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:runs" ] } ,
2094+ expirationTime : "15m" ,
2095+ } ) ;
2096+ const res = await get ( { Authorization : `Bearer ${ jwt } ` } ) ;
2097+ expect ( res . status ) . toBe ( 403 ) ;
2098+ } ) ;
2099+
2100+ it ( "JWT write:deployments (action mismatch — read route): 403" , async ( ) => {
2101+ const server = getTestServer ( ) ;
2102+ const seed = await seedTestEnvironment ( server . prisma ) ;
2103+ const jwt = await generateJWT ( {
2104+ secretKey : seed . apiKey ,
2105+ payload : { pub : true , sub : seed . environment . id , scopes : [ "write:deployments" ] } ,
2106+ expirationTime : "15m" ,
2107+ } ) ;
2108+ const res = await get ( { Authorization : `Bearer ${ jwt } ` } ) ;
2109+ expect ( res . status ) . toBe ( 403 ) ;
2110+ } ) ;
2111+ } ) ;
2112+
2113+ describe ( "Query schema — GET /api/v1/query/schema (sanity)" , ( ) => {
2114+ const path = "/api/v1/query/schema" ;
2115+
2116+ it ( "missing auth: 401" , async ( ) => {
2117+ const res = await getTestServer ( ) . webapp . fetch ( path ) ;
2118+ expect ( res . status ) . toBe ( 401 ) ;
2119+ } ) ;
2120+
2121+ it ( "JWT read:query (type-level): auth passes" , async ( ) => {
2122+ const server = getTestServer ( ) ;
2123+ const seed = await seedTestEnvironment ( server . prisma ) ;
2124+ const jwt = await generateJWT ( {
2125+ secretKey : seed . apiKey ,
2126+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:query" ] } ,
2127+ expirationTime : "15m" ,
2128+ } ) ;
2129+ const res = await server . webapp . fetch ( path , {
2130+ headers : { Authorization : `Bearer ${ jwt } ` } ,
2131+ } ) ;
2132+ expect ( res . status ) . not . toBe ( 401 ) ;
2133+ expect ( res . status ) . not . toBe ( 403 ) ;
2134+ } ) ;
2135+
2136+ it ( "JWT read:deployments (type mismatch): 403" , async ( ) => {
2137+ const server = getTestServer ( ) ;
2138+ const seed = await seedTestEnvironment ( server . prisma ) ;
2139+ const jwt = await generateJWT ( {
2140+ secretKey : seed . apiKey ,
2141+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:deployments" ] } ,
2142+ expirationTime : "15m" ,
2143+ } ) ;
2144+ const res = await server . webapp . fetch ( path , {
2145+ headers : { Authorization : `Bearer ${ jwt } ` } ,
2146+ } ) ;
2147+ expect ( res . status ) . toBe ( 403 ) ;
2148+ } ) ;
2149+ } ) ;
2150+
2151+ describe ( "Query dashboards — GET /api/v1/query/dashboards (sanity)" , ( ) => {
2152+ const path = "/api/v1/query/dashboards" ;
2153+
2154+ it ( "missing auth: 401" , async ( ) => {
2155+ const res = await getTestServer ( ) . webapp . fetch ( path ) ;
2156+ expect ( res . status ) . toBe ( 401 ) ;
2157+ } ) ;
2158+
2159+ it ( "JWT read:query: auth passes" , async ( ) => {
2160+ const server = getTestServer ( ) ;
2161+ const seed = await seedTestEnvironment ( server . prisma ) ;
2162+ const jwt = await generateJWT ( {
2163+ secretKey : seed . apiKey ,
2164+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:query" ] } ,
2165+ expirationTime : "15m" ,
2166+ } ) ;
2167+ const res = await server . webapp . fetch ( path , {
2168+ headers : { Authorization : `Bearer ${ jwt } ` } ,
2169+ } ) ;
2170+ expect ( res . status ) . not . toBe ( 401 ) ;
2171+ expect ( res . status ) . not . toBe ( 403 ) ;
2172+ } ) ;
2173+ } ) ;
2174+
2175+ describe ( "Query ad-hoc — POST /api/v1/query (body-derived resource)" , ( ) => {
2176+ const path = "/api/v1/query" ;
2177+ const post = ( body : object , headers : Record < string , string > ) =>
2178+ getTestServer ( ) . webapp . fetch ( path , {
2179+ method : "POST" ,
2180+ headers : { "Content-Type" : "application/json" , ...headers } ,
2181+ body : JSON . stringify ( body ) ,
2182+ } ) ;
2183+
2184+ it ( "missing auth: 401" , async ( ) => {
2185+ const res = await post ( { query : "SELECT * FROM runs" } , { } ) ;
2186+ expect ( res . status ) . toBe ( 401 ) ;
2187+ } ) ;
2188+
2189+ it ( "body with table 'runs' + JWT read:query:runs: auth passes (any-match)" , async ( ) => {
2190+ // detectTables pulls 'runs' from FROM-clause. Resource becomes
2191+ // [{ type: "query", id: "runs" }]. Scope read:query:runs matches.
2192+ const server = getTestServer ( ) ;
2193+ const seed = await seedTestEnvironment ( server . prisma ) ;
2194+ const jwt = await generateJWT ( {
2195+ secretKey : seed . apiKey ,
2196+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:query:runs" ] } ,
2197+ expirationTime : "15m" ,
2198+ } ) ;
2199+ const res = await post ( { query : "SELECT * FROM runs" } , {
2200+ Authorization : `Bearer ${ jwt } ` ,
2201+ } ) ;
2202+ expect ( res . status ) . not . toBe ( 401 ) ;
2203+ expect ( res . status ) . not . toBe ( 403 ) ;
2204+ } ) ;
2205+
2206+ it ( "body with no detectable tables (defaults id='all') + JWT read:query: auth passes" , async ( ) => {
2207+ // A query with no FROM clause → detectTables returns [] →
2208+ // resource is { type: "query", id: "all" }. Type-level read:query
2209+ // matches.
2210+ const server = getTestServer ( ) ;
2211+ const seed = await seedTestEnvironment ( server . prisma ) ;
2212+ const jwt = await generateJWT ( {
2213+ secretKey : seed . apiKey ,
2214+ payload : { pub : true , sub : seed . environment . id , scopes : [ "read:query" ] } ,
2215+ expirationTime : "15m" ,
2216+ } ) ;
2217+ const res = await post ( { query : "SELECT 1" } , { Authorization : `Bearer ${ jwt } ` } ) ;
2218+ expect ( res . status ) . not . toBe ( 401 ) ;
2219+ expect ( res . status ) . not . toBe ( 403 ) ;
2220+ } ) ;
2221+
2222+ it ( "body with table 'runs' + JWT read:query:other_table: 403" , async ( ) => {
2223+ const server = getTestServer ( ) ;
2224+ const seed = await seedTestEnvironment ( server . prisma ) ;
2225+ const jwt = await generateJWT ( {
2226+ secretKey : seed . apiKey ,
2227+ payload : {
2228+ pub : true ,
2229+ sub : seed . environment . id ,
2230+ scopes : [ "read:query:other_table" ] ,
2231+ } ,
2232+ expirationTime : "15m" ,
2233+ } ) ;
2234+ const res = await post ( { query : "SELECT * FROM runs" } , {
2235+ Authorization : `Bearer ${ jwt } ` ,
2236+ } ) ;
2237+ expect ( res . status ) . toBe ( 403 ) ;
2238+ } ) ;
2239+
2240+ it ( "JWT admin: auth passes regardless of body" , async ( ) => {
2241+ const server = getTestServer ( ) ;
2242+ const seed = await seedTestEnvironment ( server . prisma ) ;
2243+ const jwt = await generateJWT ( {
2244+ secretKey : seed . apiKey ,
2245+ payload : { pub : true , sub : seed . environment . id , scopes : [ "admin" ] } ,
2246+ expirationTime : "15m" ,
2247+ } ) ;
2248+ const res = await post ( { query : "SELECT * FROM runs" } , {
2249+ Authorization : `Bearer ${ jwt } ` ,
2250+ } ) ;
2251+ expect ( res . status ) . not . toBe ( 401 ) ;
2252+ expect ( res . status ) . not . toBe ( 403 ) ;
2253+ } ) ;
2254+
2255+ it ( "JWT write:query (action mismatch): 403" , async ( ) => {
2256+ const server = getTestServer ( ) ;
2257+ const seed = await seedTestEnvironment ( server . prisma ) ;
2258+ const jwt = await generateJWT ( {
2259+ secretKey : seed . apiKey ,
2260+ payload : { pub : true , sub : seed . environment . id , scopes : [ "write:query" ] } ,
2261+ expirationTime : "15m" ,
2262+ } ) ;
2263+ const res = await post ( { query : "SELECT 1" } , { Authorization : `Bearer ${ jwt } ` } ) ;
2264+ expect ( res . status ) . toBe ( 403 ) ;
2265+ } ) ;
2266+ } ) ;
2267+
20232268 describe ( "Batch retrieve — GET /realtime/v1/batches/:batchId (sanity)" , ( ) => {
20242269 it ( "missing auth: 401" , async ( ) => {
20252270 const res = await getTestServer ( ) . webapp . fetch ( "/realtime/v1/batches/batch_anything" ) ;
0 commit comments