11import * as core from "@actions/core" ;
22import * as exec from "@actions/exec" ;
33import { read , setClientInfo } from "@1password/op-js" ;
4+ import { createClient } from "@1password/sdk" ;
45import {
56 extractSecret ,
67 loadSecrets ,
@@ -22,6 +23,9 @@ jest.mock("@actions/exec", () => ({
2223 } ) ) ,
2324} ) ) ;
2425jest . mock ( "@1password/op-js" ) ;
26+ jest . mock ( "@1password/sdk" , ( ) => ( {
27+ createClient : jest . fn ( ) ,
28+ } ) ) ;
2529
2630beforeEach ( ( ) => {
2731 jest . clearAllMocks ( ) ;
@@ -143,7 +147,13 @@ describe("extractSecret", () => {
143147 } ) ;
144148} ) ;
145149
146- describe ( "loadSecrets" , ( ) => {
150+ describe ( "loadSecrets when using Connect" , ( ) => {
151+ beforeEach ( ( ) => {
152+ process . env [ envConnectHost ] = "https://localhost:8000" ;
153+ process . env [ envConnectToken ] = "token" ;
154+ process . env [ envServiceAccountToken ] = "" ;
155+ } ) ;
156+
147157 it ( "sets the client info and gets the executed output" , async ( ) => {
148158 await loadSecrets ( true ) ;
149159
@@ -181,6 +191,166 @@ describe("loadSecrets", () => {
181191 } ) ;
182192} ) ;
183193
194+ describe ( "loadSecrets when using Service Account" , ( ) => {
195+ const mockResolve = jest . fn ( ) ;
196+
197+ beforeEach ( ( ) => {
198+ process . env [ envConnectHost ] = "" ;
199+ process . env [ envConnectToken ] = "" ;
200+ process . env [ envServiceAccountToken ] = "ops_token" ;
201+
202+ Object . keys ( process . env ) . forEach ( ( key ) => {
203+ if (
204+ typeof process . env [ key ] === "string" &&
205+ process . env [ key ] ?. startsWith ( "op://" )
206+ ) {
207+ delete process . env [ key ] ;
208+ }
209+ } ) ;
210+ process . env . MY_SECRET = "op://vault/item/field" ;
211+
212+ ( createClient as jest . Mock ) . mockResolvedValue ( {
213+ secrets : { resolve : mockResolve } ,
214+ } ) ;
215+
216+ mockResolve . mockResolvedValue ( "resolved-secret-value" ) ;
217+ } ) ;
218+
219+ it ( "does not call op env ls when using Service Account" , async ( ) => {
220+ await loadSecrets ( false ) ;
221+ expect ( exec . getExecOutput ) . not . toHaveBeenCalled ( ) ;
222+ } ) ;
223+
224+ it ( "sets step output with resolved value when export-env is false" , async ( ) => {
225+ await loadSecrets ( false ) ;
226+ expect ( core . setOutput ) . toHaveBeenCalledTimes ( 1 ) ;
227+ expect ( core . setOutput ) . toHaveBeenCalledWith (
228+ "MY_SECRET" ,
229+ "resolved-secret-value" ,
230+ ) ;
231+ } ) ;
232+
233+ it ( "masks secret with setSecret when export-env is false" , async ( ) => {
234+ await loadSecrets ( false ) ;
235+ expect ( core . setSecret ) . toHaveBeenCalledTimes ( 1 ) ;
236+ expect ( core . setSecret ) . toHaveBeenCalledWith ( "resolved-secret-value" ) ;
237+ } ) ;
238+
239+ it ( "does not call exportVariable when export-env is false" , async ( ) => {
240+ await loadSecrets ( false ) ;
241+ expect ( core . exportVariable ) . not . toHaveBeenCalled ( ) ;
242+ } ) ;
243+
244+ it ( "exports env and sets OP_MANAGED_VARIABLES when export-env is true" , async ( ) => {
245+ await loadSecrets ( true ) ;
246+ expect ( core . exportVariable ) . toHaveBeenCalledWith (
247+ "MY_SECRET" ,
248+ "resolved-secret-value" ,
249+ ) ;
250+ expect ( core . exportVariable ) . toHaveBeenCalledWith (
251+ envManagedVariables ,
252+ "MY_SECRET" ,
253+ ) ;
254+ } ) ;
255+
256+ it ( "does not set step output when export-env is true" , async ( ) => {
257+ await loadSecrets ( true ) ;
258+ expect ( core . setOutput ) . not . toHaveBeenCalledWith (
259+ "MY_SECRET" ,
260+ expect . anything ( ) ,
261+ ) ;
262+ } ) ;
263+
264+ it ( "masks secret with setSecret when export-env is true" , async ( ) => {
265+ await loadSecrets ( true ) ;
266+ expect ( core . setSecret ) . toHaveBeenCalledTimes ( 1 ) ;
267+ expect ( core . setSecret ) . toHaveBeenCalledWith ( "resolved-secret-value" ) ;
268+ } ) ;
269+
270+ it ( "returns early when no env vars have op:// refs" , async ( ) => {
271+ Object . keys ( process . env ) . forEach ( ( key ) => {
272+ if (
273+ typeof process . env [ key ] === "string" &&
274+ process . env [ key ] ?. startsWith ( "op://" )
275+ ) {
276+ delete process . env [ key ] ;
277+ }
278+ } ) ;
279+ await loadSecrets ( true ) ;
280+ expect ( exec . getExecOutput ) . not . toHaveBeenCalled ( ) ;
281+ expect ( core . exportVariable ) . not . toHaveBeenCalled ( ) ;
282+ } ) ;
283+
284+ it ( "wraps createClient errors with a descriptive message" , async ( ) => {
285+ ( createClient as jest . Mock ) . mockRejectedValue (
286+ new Error ( "invalid token format" ) ,
287+ ) ;
288+ await expect ( loadSecrets ( false ) ) . rejects . toThrow (
289+ "Service account authentication failed: invalid token format" ,
290+ ) ;
291+ } ) ;
292+
293+ describe ( "multiple refs" , ( ) => {
294+ const ref1 = "op://vault/item/field" ;
295+ const ref2 = "op://vault/other/item" ;
296+ const ref3 = "op://vault/file/secret" ;
297+
298+ beforeEach ( ( ) => {
299+ process . env . MY_SECRET = ref1 ;
300+ process . env . ANOTHER_SECRET = ref2 ;
301+ process . env . FILE_SECRET = ref3 ;
302+
303+ mockResolve
304+ . mockResolvedValueOnce ( "value1" )
305+ . mockResolvedValueOnce ( "value2" )
306+ . mockResolvedValueOnce ( "value3" ) ;
307+ } ) ;
308+
309+ it ( "resolves each ref and sets step output for each when export-env is false" , async ( ) => {
310+ await loadSecrets ( false ) ;
311+
312+ expect ( mockResolve ) . toHaveBeenCalledTimes ( 3 ) ;
313+ expect ( mockResolve ) . toHaveBeenCalledWith ( ref1 ) ;
314+ expect ( mockResolve ) . toHaveBeenCalledWith ( ref2 ) ;
315+ expect ( mockResolve ) . toHaveBeenCalledWith ( ref3 ) ;
316+
317+ expect ( core . setOutput ) . toHaveBeenCalledTimes ( 3 ) ;
318+ expect ( core . setOutput ) . toHaveBeenCalledWith ( "MY_SECRET" , "value1" ) ;
319+ expect ( core . setOutput ) . toHaveBeenCalledWith ( "ANOTHER_SECRET" , "value2" ) ;
320+ expect ( core . setOutput ) . toHaveBeenCalledWith ( "FILE_SECRET" , "value3" ) ;
321+
322+ expect ( core . setSecret ) . toHaveBeenCalledTimes ( 3 ) ;
323+ } ) ;
324+
325+ it ( "resolves each ref and exports each and sets OP_MANAGED_VARIABLES when export-env is true" , async ( ) => {
326+ await loadSecrets ( true ) ;
327+
328+ expect ( mockResolve ) . toHaveBeenCalledTimes ( 3 ) ;
329+
330+ expect ( core . exportVariable ) . toHaveBeenCalledWith ( "MY_SECRET" , "value1" ) ;
331+ expect ( core . exportVariable ) . toHaveBeenCalledWith (
332+ "ANOTHER_SECRET" ,
333+ "value2" ,
334+ ) ;
335+ expect ( core . exportVariable ) . toHaveBeenCalledWith ( "FILE_SECRET" , "value3" ) ;
336+
337+ const exportVariableCalls = ( core . exportVariable as jest . Mock ) . mock
338+ . calls as [ string , string ] [ ] ;
339+ const managedVarsCall = exportVariableCalls . find (
340+ ( [ name ] ) => name === envManagedVariables ,
341+ ) ;
342+ expect ( managedVarsCall ) . toBeDefined ( ) ;
343+ const managedList = ( managedVarsCall as [ string , string ] ) [ 1 ] . split ( "," ) ;
344+ expect ( managedList ) . toContain ( "MY_SECRET" ) ;
345+ expect ( managedList ) . toContain ( "ANOTHER_SECRET" ) ;
346+ expect ( managedList ) . toContain ( "FILE_SECRET" ) ;
347+ expect ( managedList ) . toHaveLength ( 3 ) ;
348+
349+ expect ( core . setSecret ) . toHaveBeenCalledTimes ( 3 ) ;
350+ } ) ;
351+ } ) ;
352+ } ) ;
353+
184354describe ( "unsetPrevious" , ( ) => {
185355 const testManagedEnv = "TEST_SECRET" ;
186356 const testSecretValue = "MyS3cr#T" ;
0 commit comments