@@ -2,6 +2,8 @@ package age
22
33import (
44 "fmt"
5+ "os"
6+ "path/filepath"
57 "testing"
68
79 "filippo.io/age"
@@ -107,3 +109,112 @@ func TestIdentityAndRecipient(t *testing.T) {
107109 })
108110 }
109111}
112+
113+ // newTestAge creates an Age instance whose identity file lives under a temp
114+ // directory and uses a fixed test passphrase so no interactive prompt is
115+ // needed during tests.
116+ func newTestAge (t * testing.T ) * Age {
117+ t .Helper ()
118+ td := t .TempDir ()
119+ t .Setenv ("GOPASS_HOMEDIR" , td )
120+ ctx := t .Context ()
121+ a , err := New (ctx , "" )
122+ require .NoError (t , err )
123+ // Override the identity path to a known temp location.
124+ a .identity = filepath .Join (td , "age" , "identities" )
125+ // Use a fixed passphrase so no UI interaction is needed.
126+ a .pwCallback = func (_ string , _ bool ) ([]byte , error ) {
127+ return []byte ("test-passphrase" ), nil
128+ }
129+ a .pwPurgeCallback = func (_ string ) {}
130+
131+ return a
132+ }
133+
134+ // TestAddIdentityToNewFile verifies that addIdentity works when no identity
135+ // file exists yet (i.e. creates a new file correctly).
136+ func TestAddIdentityToNewFile (t * testing.T ) {
137+ ctx := t .Context ()
138+ a := newTestAge (t )
139+
140+ id , err := age .GenerateX25519Identity ()
141+ require .NoError (t , err )
142+
143+ require .NoError (t , a .addIdentity (ctx , id ))
144+
145+ // The file should now exist and contain exactly the one identity.
146+ ids , err := a .Identities (ctx )
147+ require .NoError (t , err )
148+ require .Len (t , ids , 1 )
149+ rec := IdentityToRecipient (ids [0 ])
150+ require .NotNil (t , rec )
151+ assert .Equal (t , id .Recipient ().String (), fmt .Sprintf ("%s" , rec ))
152+ }
153+
154+ // TestAddIdentityDoesNotParseExistingPluginLines verifies Option A: when a
155+ // plugin-format line is already present in the identity file, adding a new
156+ // native identity does NOT re-invoke the plugin binary (because we only do a
157+ // raw text append, not a parse-all-then-serialize cycle).
158+ //
159+ // We simulate this by writing a plugin-format raw line directly into the
160+ // encrypted identity file via saveIdentities, then adding a new key. If the
161+ // old code path were still active it would call parseIdentity on the plugin
162+ // line, which calls plugin.NewIdentity() and would fail without the binary.
163+ // With Option A, the plugin line is copied verbatim.
164+ func TestAddIdentityPreservesPluginLineWithoutInvokingPlugin (t * testing.T ) {
165+ ctx := t .Context ()
166+ a := newTestAge (t )
167+
168+ // Plant a plugin-format line (gopass custom format: identity|recipient).
169+ // This is the serialized form that saveIdentities / identitiesToString
170+ // would produce for a wrappedIdentity.
171+ pluginRaw := "AGE-PLUGIN-YUBIKEY-1GKZKJQYZL98RLMC67F9PJ|age1yubikey1qt2r3tfk7wvlykudm7ew28dqqm3h8ln9zfsxsq4lcd2w8rh4n4hhz46ur24"
172+ require .NoError (t , a .saveIdentities (ctx , []string {pluginRaw }, true ))
173+
174+ // Now add a real native identity. Option A must NOT try to call
175+ // plugin.NewIdentity() on the existing pluginRaw line.
176+ newID , err := age .GenerateX25519Identity ()
177+ require .NoError (t , err )
178+ require .NoError (t , a .addIdentity (ctx , newID ))
179+
180+ // Read back the raw file and confirm both lines are present.
181+ raw , err := a .loadIdentityFile (ctx )
182+ require .NoError (t , err )
183+
184+ assert .Contains (t , raw , pluginRaw , "plugin line must be preserved verbatim" )
185+ assert .Contains (t , raw , newID .String (), "new native identity must be appended" )
186+ }
187+
188+ // TestAddMultipleIdentitiesAccumulate verifies that calling addIdentity
189+ // multiple times accumulates all identities in the file, each as its own line.
190+ func TestAddMultipleIdentitiesAccumulate (t * testing.T ) {
191+ ctx := t .Context ()
192+ a := newTestAge (t )
193+
194+ id1 , err := age .GenerateX25519Identity ()
195+ require .NoError (t , err )
196+ id2 , err := age .GenerateX25519Identity ()
197+ require .NoError (t , err )
198+ id3 , err := age .GenerateX25519Identity ()
199+ require .NoError (t , err )
200+
201+ require .NoError (t , a .addIdentity (ctx , id1 ))
202+ require .NoError (t , a .addIdentity (ctx , id2 ))
203+ require .NoError (t , a .addIdentity (ctx , id3 ))
204+
205+ ids , err := a .Identities (ctx )
206+ require .NoError (t , err )
207+ require .Len (t , ids , 3 )
208+ }
209+
210+ // TestLoadIdentityFileNotExist verifies that loadIdentityFile returns an
211+ // os.ErrNotExist-wrapped error when the identity file has not been created yet.
212+ func TestLoadIdentityFileNotExist (t * testing.T ) {
213+ a := newTestAge (t )
214+ ctx := t .Context ()
215+
216+ _ , err := a .loadIdentityFile (ctx )
217+ require .Error (t , err )
218+ assert .ErrorIs (t , err , os .ErrNotExist ,
219+ "loadIdentityFile should surface an os.ErrNotExist-compatible error" )
220+ }
0 commit comments