@@ -6,12 +6,14 @@ import test, { ExecutionContext } from "ava";
66import * as yaml from "js-yaml" ;
77import * as sinon from "sinon" ;
88
9+ import { ActionState } from "./action-common" ;
910import * as actionsUtil from "./actions-util" ;
1011import { AnalysisKind , supportedAnalysisKinds } from "./analyses" ;
1112import * as api from "./api-client" ;
1213import { CachingKind } from "./caching-utils" ;
1314import { createStubCodeQL } from "./codeql" ;
1415import { UserConfig } from "./config/db-config" ;
16+ import * as file from "./config/file" ;
1517import * as configUtils from "./config-utils" ;
1618import * as errorMessages from "./error-messages" ;
1719import { Feature } from "./feature-flags" ;
@@ -38,6 +40,8 @@ import {
3840 makeMacro ,
3941 initAllState ,
4042 callee ,
43+ SAMPLE_DOTCOM_API_DETAILS ,
44+ AssertableTarget ,
4145} from "./testing-utils" ;
4246import {
4347 GitHubVariant ,
@@ -2529,3 +2533,177 @@ test("determineUserConfig - ignores config file input outside Default Setup if F
25292533 } ) ;
25302534 } ) ;
25312535} ) ;
2536+
2537+ test ( "loadUserConfig - loads local configuration files" , async ( t ) => {
2538+ await withTmpDir ( async ( workspaceDir ) => {
2539+ await withTmpDir ( async ( tmpDir ) => {
2540+ // Construct the test target.
2541+ const loadUserConfig = (
2542+ actionState : ActionState < [ "Logger" , "Env" , "FeatureFlags" ] > ,
2543+ filePath : string ,
2544+ ) =>
2545+ configUtils . loadUserConfig (
2546+ actionState ,
2547+ filePath ,
2548+ workspaceDir ,
2549+ SAMPLE_DOTCOM_API_DETAILS ,
2550+ tmpDir ,
2551+ ) ;
2552+ const target = callee ( loadUserConfig ) ;
2553+
2554+ // `loadUserConfig` should load local configuration files if they are inside the workspace:
2555+ const insideOfWorkspace = path . join ( workspaceDir , "some-file.yml" ) ;
2556+ fs . writeFileSync ( insideOfWorkspace , "test-key: present" , "utf8" ) ;
2557+
2558+ await target
2559+ . withArgs ( insideOfWorkspace )
2560+ . passes ( t . deepEqual , { "test-key" : "present" } ) ;
2561+
2562+ // `loadUserConfig` should normally throw if the path is outside of the workspace:
2563+ const outsideOfWorkspace = path . join (
2564+ tmpDir ,
2565+ "not-the-generated-file.yml" ,
2566+ ) ;
2567+ fs . writeFileSync ( outsideOfWorkspace , "test-key: present" , "utf8" ) ;
2568+
2569+ await target
2570+ . withArgs ( outsideOfWorkspace )
2571+ . throws ( t , { instanceOf : ConfigurationError } ) ;
2572+
2573+ // `loadUserConfig` does not throw if the path is the result of `userConfigFromActionPath`:
2574+ const generatedPath = configUtils . userConfigFromActionPath ( tmpDir ) ;
2575+ fs . writeFileSync ( generatedPath , "test-key: present" , "utf8" ) ;
2576+
2577+ await target
2578+ . withArgs ( generatedPath )
2579+ . passes ( t . deepEqual , { "test-key" : "present" } ) ;
2580+ } ) ;
2581+ } ) ;
2582+ } ) ;
2583+
2584+ test . serial ( "loadUserConfig - loads remote configuration files" , async ( t ) => {
2585+ await withTmpDir ( async ( tmpDir ) => {
2586+ const getRemoteConfig = sinon . stub ( file , "getRemoteConfig" ) . resolves ( { } ) ;
2587+
2588+ const remoteAddress = "owner/repo/file@ref" ;
2589+ await callee ( configUtils . loadUserConfig )
2590+ . withArgs ( remoteAddress , tmpDir , SAMPLE_DOTCOM_API_DETAILS , tmpDir )
2591+ . passes ( t . deepEqual , { } ) ;
2592+
2593+ t . true (
2594+ getRemoteConfig . calledOnceWithExactly (
2595+ sinon . match . any ,
2596+ remoteAddress ,
2597+ SAMPLE_DOTCOM_API_DETAILS ,
2598+ ) ,
2599+ ) ;
2600+ } ) ;
2601+ } ) ;
2602+
2603+ test . serial (
2604+ "loadUserConfig - loads remote configuration files (new format, partial)" ,
2605+ async ( t ) => {
2606+ await withTmpDir ( async ( tmpDir ) => {
2607+ const getRemoteConfig = sinon . stub ( file , "getRemoteConfig" ) . resolves ( { } ) ;
2608+
2609+ // Construct the basic test target.
2610+ const target = callee ( configUtils . loadUserConfig )
2611+ . withDefaultActionsEnv ( )
2612+ . withFeatures ( [ Feature . NewRemoteFileAddresses ] ) ;
2613+
2614+ // Utility function to assert that `targetWithArgs` has identified
2615+ // the input as a remote file address.
2616+ const checkIsRemote =
2617+ ( address : string ) =>
2618+ async < R > ( targetWithArgs : AssertableTarget < R > ) => {
2619+ // We have stubbed `getRemoteConfig` to resolve to `{}`, so we
2620+ // expect that result.
2621+ await targetWithArgs . passes ( t . deepEqual , { } ) ;
2622+
2623+ // And `getRemoteConfig` should have been called exactly once.
2624+ t . is ( getRemoteConfig . callCount , 1 ) ;
2625+
2626+ // Get the arguments for the call and check that there were three.
2627+ // We don't care about the first, but check that the other two
2628+ // match our expectations. We break it down like this to get
2629+ // more useful test output.
2630+ const args = getRemoteConfig . getCalls ( ) [ 0 ] . args ;
2631+ t . is ( args . length , 3 ) ;
2632+ t . deepEqual ( args [ 1 ] , address ) ;
2633+ t . deepEqual ( args [ 2 ] , SAMPLE_DOTCOM_API_DETAILS ) ;
2634+ } ;
2635+
2636+ // Utility function to assert that `targetWithArgs` has not identified
2637+ // the input as a remote file address.
2638+ const checkIsNotRemote = async < R > (
2639+ targetWithArgs : AssertableTarget < R > ,
2640+ ) => {
2641+ // We expect `loadUserConfig` to have thrown if it thinks the path is local,
2642+ // since the inputs we provide aren't for files that exist.
2643+ await targetWithArgs . throws ( t ) ;
2644+
2645+ // Additionally, we expect that `getRemoteConfig` wasn't called.
2646+ t . is ( getRemoteConfig . callCount , 0 ) ;
2647+ } ;
2648+
2649+ // Utility function to add the explicit `REMOTE_PATH_PREFIX` to the input.
2650+ const withExplicitPrefix = ( str : string ) =>
2651+ `${ file . REMOTE_PATH_PREFIX } ${ str } ` ;
2652+
2653+ // Utility to set up a call to `loadUserConfig` with the provided `address`
2654+ // and pass it to `assertion`.
2655+ const testTargetWith = async (
2656+ address : string ,
2657+ assertion : (
2658+ targetWithArgs : AssertableTarget < Promise < UserConfig > > ,
2659+ ) => Promise < any > ,
2660+ ) => {
2661+ // Reset the stub's history since we re-use it.
2662+ getRemoteConfig . resetHistory ( ) ;
2663+
2664+ // Log the input we are testing so that, in the event of a failure,
2665+ // it is easier to see which input was responsible.
2666+ t . log ( `testTargetWith("${ address } ")` ) ;
2667+
2668+ // Prepare the test call to `loadUserConfig`.
2669+ const targetWithArgs = target . withArgs (
2670+ address ,
2671+ tmpDir ,
2672+ SAMPLE_DOTCOM_API_DETAILS ,
2673+ tmpDir ,
2674+ ) ;
2675+
2676+ // Pass it to the provided assertion function.
2677+ await assertion ( targetWithArgs ) ;
2678+ } ;
2679+
2680+ // Since this input contains an '@' character, it is treated as a remote path
2681+ // by the old logic even without the explicit prefix.
2682+ const remoteWithoutPrefix = "repo@main" ;
2683+ await testTargetWith (
2684+ remoteWithoutPrefix ,
2685+ checkIsRemote ( remoteWithoutPrefix ) ,
2686+ ) ;
2687+ await testTargetWith (
2688+ withExplicitPrefix ( remoteWithoutPrefix ) ,
2689+ checkIsRemote ( remoteWithoutPrefix ) ,
2690+ ) ;
2691+ // It is only treated as a local path with the corresponding prefix.
2692+ await testTargetWith ( `./${ remoteWithoutPrefix } ` , checkIsNotRemote ) ;
2693+
2694+ // The following test inputs are examples of ambiguous paths. They could refer to
2695+ // valid local or remote paths. For each, we check that they are treated as remote
2696+ // paths if the explicit remote file prefix is used and as local paths otherwise.
2697+ const testInputs = [ "repo:file" , "input" , "../input" ] ;
2698+
2699+ for ( const testInput of testInputs ) {
2700+ for ( const addPrefix of [ true , false ] ) {
2701+ await testTargetWith (
2702+ addPrefix ? withExplicitPrefix ( testInput ) : testInput ,
2703+ addPrefix ? checkIsRemote ( testInput ) : checkIsNotRemote ,
2704+ ) ;
2705+ }
2706+ }
2707+ } ) ;
2708+ } ,
2709+ ) ;
0 commit comments