@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
44// Use vi.hoisted to ensure mocks are set up before module imports
55const mockQuery = vi . hoisted ( ( ) => vi . fn ( ) ) ;
66const mockGetPresignedUrl = vi . hoisted ( ( ) => vi . fn ( ) ) ;
7+ const mockIsMember = vi . hoisted ( ( ) => vi . fn ( ) ) ;
78
89vi . mock ( "pg" , ( ) => {
910 return {
@@ -18,6 +19,17 @@ vi.mock("../utils/s3", () => ({
1819 getPresignedDocsAssetsDownloadUrl : mockGetPresignedUrl
1920} ) ) ;
2021
22+ vi . mock ( "@fern-api/venus-api-sdk" , ( ) => ( {
23+ FernVenusApiClient : vi . fn ( ( ) => ( {
24+ organization : {
25+ isMember : mockIsMember
26+ }
27+ } ) ) ,
28+ FernVenusApi : {
29+ OrganizationId : ( id : string ) => id
30+ }
31+ } ) ) ;
32+
2133// Import handler after mocks are configured
2234import { handler } from "../index" ;
2335
@@ -27,16 +39,21 @@ describe("Lambda Handler", () => {
2739 vi . clearAllMocks ( ) ;
2840 mockQuery . mockReset ( ) ;
2941 mockGetPresignedUrl . mockReset ( ) ;
42+ mockIsMember . mockReset ( ) ;
3043 // Default S3 mock to return a URL
3144 mockGetPresignedUrl . mockResolvedValue ( "https://s3.example.com/file.png" ) ;
45+ // Default Venus mock to allow access (member of fern org)
46+ mockIsMember . mockResolvedValue ( { ok : true , body : true } ) ;
47+ // Set VENUS_URL for tests
48+ process . env . VENUS_URL = "https://venus.buildwithfern.com" ;
3249 } ) ;
3350
34- const createMockEvent = ( path : string , method : string , body ?: any ) : APIGatewayProxyEvent => {
51+ const createMockEvent = ( path : string , method : string , body ?: any , headers ?: Record < string , string > ) : APIGatewayProxyEvent => {
3552 return {
3653 path,
3754 httpMethod : method ,
3855 body : body ? JSON . stringify ( body ) : null ,
39- headers : { } ,
56+ headers : headers || { } ,
4057 multiValueHeaders : { } ,
4158 isBase64Encoded : false ,
4259 pathParameters : null ,
@@ -305,7 +322,7 @@ describe("Lambda Handler", () => {
305322 } ) ;
306323
307324 describe ( "POST /load-docs-for-url" , ( ) => {
308- it ( "should return docs for a valid URL" , async ( ) => {
325+ it ( "should return docs for a valid URL with auth " , async ( ) => {
309326 const mockDocsDefinition = Buffer . from (
310327 JSON . stringify ( {
311328 type : "v3" ,
@@ -345,6 +362,8 @@ describe("Lambda Handler", () => {
345362
346363 const event = createMockEvent ( "/load-docs-for-url" , "POST" , {
347364 url : "https://docs.example.com"
365+ } , {
366+ Authorization : "Bearer test-token"
348367 } ) ;
349368 const context = createMockContext ( ) ;
350369
@@ -390,6 +409,8 @@ describe("Lambda Handler", () => {
390409
391410 const event = createMockEvent ( "/v2/registry/docs/load-docs-for-url" , "POST" , {
392411 url : "docs.test.com"
412+ } , {
413+ Authorization : "Bearer test-token"
393414 } ) ;
394415 const context = createMockContext ( ) ;
395416
@@ -429,6 +450,89 @@ describe("Lambda Handler", () => {
429450 expect ( body . error ) . toBe ( "InvalidUrlError" ) ;
430451 } ) ;
431452
453+ it ( "should return 401 when authorization header is missing" , async ( ) => {
454+ const mockDocsDefinition = Buffer . from (
455+ JSON . stringify ( {
456+ type : "v3" ,
457+ pages : { } ,
458+ config : {
459+ navigation : { items : [ ] } ,
460+ colorsV3 : { type : "light" }
461+ } ,
462+ files : { } ,
463+ referencedApis : [ ]
464+ } )
465+ ) ;
466+
467+ mockQuery . mockResolvedValueOnce ( {
468+ rows : [
469+ {
470+ orgID : "test-org" ,
471+ domain : "docs.example.com" ,
472+ path : "" ,
473+ docsDefinition : mockDocsDefinition ,
474+ docsConfigInstanceId : "config-123" ,
475+ authType : "PUBLIC" ,
476+ hasPublicS3Assets : true
477+ }
478+ ]
479+ } ) ;
480+
481+ const event = createMockEvent ( "/load-docs-for-url" , "POST" , {
482+ url : "https://docs.example.com"
483+ } ) ; // No auth header
484+ const context = createMockContext ( ) ;
485+
486+ const result = await handler ( event , context ) ;
487+
488+ expect ( result . statusCode ) . toBe ( 401 ) ;
489+ expect ( JSON . parse ( result . body ) . error ) . toBe ( "UnauthorizedError" ) ;
490+ } ) ;
491+
492+ it ( "should return 403 when user is not in the org" , async ( ) => {
493+ const mockDocsDefinition = Buffer . from (
494+ JSON . stringify ( {
495+ type : "v3" ,
496+ pages : { } ,
497+ config : {
498+ navigation : { items : [ ] } ,
499+ colorsV3 : { type : "light" }
500+ } ,
501+ files : { } ,
502+ referencedApis : [ ]
503+ } )
504+ ) ;
505+
506+ mockQuery . mockResolvedValueOnce ( {
507+ rows : [
508+ {
509+ orgID : "test-org" ,
510+ domain : "docs.example.com" ,
511+ path : "" ,
512+ docsDefinition : mockDocsDefinition ,
513+ docsConfigInstanceId : "config-123" ,
514+ authType : "PUBLIC" ,
515+ hasPublicS3Assets : true
516+ }
517+ ]
518+ } ) ;
519+
520+ // Mock Venus to deny access (not in fern org, not in specific org)
521+ mockIsMember . mockResolvedValue ( { ok : true , body : false } ) ;
522+
523+ const event = createMockEvent ( "/load-docs-for-url" , "POST" , {
524+ url : "https://docs.example.com"
525+ } , {
526+ Authorization : "Bearer test-token"
527+ } ) ;
528+ const context = createMockContext ( ) ;
529+
530+ const result = await handler ( event , context ) ;
531+
532+ expect ( result . statusCode ) . toBe ( 403 ) ;
533+ expect ( JSON . parse ( result . body ) . error ) . toBe ( "UserNotInOrgError" ) ;
534+ } ) ;
535+
432536 it ( "should return 404 when domain is not registered" , async ( ) => {
433537 // Mock empty DocsV2 result and empty V1 Docs result (fallback)
434538 mockQuery
@@ -437,6 +541,8 @@ describe("Lambda Handler", () => {
437541
438542 const event = createMockEvent ( "/load-docs-for-url" , "POST" , {
439543 url : "https://unknown.example.com"
544+ } , {
545+ Authorization : "Bearer test-token"
440546 } ) ;
441547 const context = createMockContext ( ) ;
442548
@@ -516,6 +622,8 @@ describe("Lambda Handler", () => {
516622
517623 const event = createMockEvent ( "/load-docs-for-url" , "POST" , {
518624 url : "https://docs.example.com"
625+ } , {
626+ Authorization : "Bearer test-token"
519627 } ) ;
520628 const context = createMockContext ( ) ;
521629
0 commit comments