@@ -2,6 +2,7 @@ package config
22
33import (
44 "context"
5+ "encoding/json"
56 "os"
67 "path/filepath"
78 "testing"
@@ -19,6 +20,21 @@ func writeKeyFile(t *testing.T, content string) string {
1920 return path
2021}
2122
23+ // writeAPIKeyConfig writes a minimal mcp_config.json whose api_key is apiKey.
24+ // The JSON is built with encoding/json, never by formatting apiKey into a
25+ // string: a reference such as ${file:C:\Users\x\key} carries backslashes that
26+ // a Windows path puts into the document and that must be escaped.
27+ func writeAPIKeyConfig (t * testing.T , apiKey string ) string {
28+ t .Helper ()
29+ raw , err := json .Marshal (map [string ]any {
30+ "listen" : "127.0.0.1:8080" ,
31+ "api_key" : apiKey ,
32+ "mcpServers" : []any {},
33+ })
34+ require .NoError (t , err )
35+ return writeOverrideTestFile (t , string (raw ))
36+ }
37+
2238// unsetEnv removes name for the test and restores it afterwards (t.Setenv
2339// records the original value; the Unsetenv that follows leaves it unset).
2440func unsetEnv (t * testing.T , name string ) {
@@ -275,7 +291,7 @@ func TestLoad_APIKeyReferenceResolvesAsProcessOverride(t *testing.T) {
275291 ResetProcessOverrides ()
276292 keyFile := writeKeyFile (t , "ref-secret-key\r \n " )
277293 ref := "${file:" + keyFile + "}"
278- path := writeOverrideTestFile (t , `{"listen": "127.0.0.1:8080", "api_key": "` + ref + `", "mcpServers": []}` )
294+ path := writeAPIKeyConfig (t , ref )
279295 unsetEnv (t , EnvAPIKey )
280296 t .Setenv (EnvAPIKeyFile , "" )
281297
@@ -307,7 +323,7 @@ func TestPersistableConfig_APIKeyReferenceSurvivesAnUnrelatedEdit(t *testing.T)
307323 ResetProcessOverrides ()
308324 keyFile := writeKeyFile (t , "ref-secret-key\n " )
309325 ref := "${file:" + keyFile + "}"
310- path := writeOverrideTestFile (t , `{"listen": "127.0.0.1:8080", "api_key": "` + ref + `", "mcpServers": []}` )
326+ path := writeAPIKeyConfig (t , ref )
311327 unsetEnv (t , EnvAPIKey )
312328 t .Setenv (EnvAPIKeyFile , "" )
313329
@@ -325,7 +341,7 @@ func TestPersistableConfig_EditingAPIKeyUnderAReferenceIsPersisted(t *testing.T)
325341 t .Cleanup (ResetProcessOverrides )
326342 ResetProcessOverrides ()
327343 keyFile := writeKeyFile (t , "ref-secret-key\n " )
328- path := writeOverrideTestFile (t , `{"listen": "127.0.0.1:8080", "api_key": " ${file:` + keyFile + `}", "mcpServers": []}` )
344+ path := writeAPIKeyConfig (t , " ${file:" + keyFile + "}" )
329345 unsetEnv (t , EnvAPIKey )
330346 t .Setenv (EnvAPIKeyFile , "" )
331347
@@ -348,16 +364,15 @@ func TestLoad_APIKeyReferenceThatCannotBeResolvedFailsTheLoad(t *testing.T) {
348364 t .Run (name , func (t * testing.T ) {
349365 t .Cleanup (ResetProcessOverrides )
350366 ResetProcessOverrides ()
351- path := writeOverrideTestFile (t , `{"listen": "127.0.0.1:8080", "api_key": "` + ref + `", "mcpServers": []}` )
367+ path := writeAPIKeyConfig (t , ref )
352368 unsetEnv (t , EnvAPIKey )
353369 t .Setenv (EnvAPIKeyFile , "" )
354370
355371 _ , err := LoadFromFile (path )
356372 require .Error (t , err , "no key may be generated over a reference that does not resolve" )
357373 assert .Contains (t , err .Error (), "api_key" )
358- raw , readErr := os .ReadFile (path )
359- require .NoError (t , readErr )
360- assert .Contains (t , string (raw ), ref , "the file is untouched" )
374+ assert .NotContains (t , err .Error (), ref , "the error never quotes the reference" )
375+ assert .Equal (t , ref , readJSON (t , path )["api_key" ], "the file is untouched" )
361376 })
362377 }
363378}
@@ -368,7 +383,7 @@ func TestLoad_EnvironmentKeyWinsOverAnAPIKeyReferenceWithoutReadingIt(t *testing
368383 // The referenced file does not exist: with an env key the reference is not
369384 // resolved at all, so that is not an error.
370385 ref := "${file:" + filepath .Join (t .TempDir (), "missing" ) + "}"
371- path := writeOverrideTestFile (t , `{"listen": "127.0.0.1:8080", "api_key": "` + ref + `", "mcpServers": []}` )
386+ path := writeAPIKeyConfig (t , ref )
372387 t .Setenv (EnvAPIKey , "env-key" )
373388 t .Setenv (EnvAPIKeyFile , "" )
374389
@@ -405,3 +420,27 @@ func TestServerConfig_FileReferencesExpandOnACopy(t *testing.T) {
405420 assert .Contains (t , orig .Env ["GH_TOKEN" ], "${file:" , "the stored config keeps the reference" )
406421 assert .Contains (t , orig .Headers ["Authorization" ], "${file:" )
407422}
423+
424+ // Both variables set stays an error when api_key is a reference: the reference
425+ // is not read, and the error is the same fixed text.
426+ func TestLoad_BothSetFailsTheLoadEvenWithAnAPIKeyReference (t * testing.T ) {
427+ t .Cleanup (ResetProcessOverrides )
428+ ResetProcessOverrides ()
429+ ref := "${file:" + writeKeyFile (t , "ref-secret-key\n " ) + "}"
430+ path := writeAPIKeyConfig (t , ref )
431+ t .Setenv (EnvAPIKey , "env-secret-value-1234" )
432+ t .Setenv (EnvAPIKeyFile , writeKeyFile (t , "file-secret-value-5678\n " ))
433+
434+ _ , err := LoadFromFile (path )
435+ require .ErrorIs (t , err , ErrAPIKeyBothSet )
436+ assert .NotContains (t , err .Error (), ref )
437+ assert .NotContains (t , err .Error (), "ref-secret-key" )
438+ }
439+
440+ // The helper must produce valid JSON for a Windows-style path, which is what
441+ // the tests above feed it when they run on Windows.
442+ func TestWriteAPIKeyConfig_EscapesBackslashes (t * testing.T ) {
443+ ref := `${file:C:\Users\ci\AppData\Local\Temp\key}`
444+ path := writeAPIKeyConfig (t , ref )
445+ assert .Equal (t , ref , readJSON (t , path )["api_key" ])
446+ }
0 commit comments