@@ -17,11 +17,13 @@ package create
1717import (
1818 "fmt"
1919 "net/http"
20+ "os"
2021 "path/filepath"
2122 "testing"
2223
2324 "github.com/slackapi/slack-cli/internal/config"
2425 "github.com/slackapi/slack-cli/internal/experiment"
26+ "github.com/slackapi/slack-cli/internal/logger"
2527 "github.com/slackapi/slack-cli/internal/shared"
2628 "github.com/slackapi/slack-cli/internal/slackcontext"
2729 "github.com/slackapi/slack-cli/internal/slackhttp"
@@ -183,6 +185,128 @@ func TestCreateGitArgs(t *testing.T) {
183185 assert .Equal (t , expectedArgs , testGitArgs )
184186}
185187
188+ func TestNormalizeSubdir (t * testing.T ) {
189+ tests := map [string ]struct {
190+ input string
191+ expected string
192+ expectError bool
193+ }{
194+ "empty string returns empty" : {
195+ input : "" ,
196+ expected : "" ,
197+ },
198+ "dot returns empty" : {
199+ input : "." ,
200+ expected : "" ,
201+ },
202+ "slash returns empty" : {
203+ input : "/" ,
204+ expected : "" ,
205+ },
206+ "simple subdir" : {
207+ input : "pydantic-ai/" ,
208+ expected : "pydantic-ai" ,
209+ },
210+ "dot-prefixed subdir" : {
211+ input : "./my-app" ,
212+ expected : "my-app" ,
213+ },
214+ "nested subdir" : {
215+ input : "apps/my-app" ,
216+ expected : "apps/my-app" ,
217+ },
218+ "parent traversal is rejected" : {
219+ input : "../escape" ,
220+ expectError : true ,
221+ },
222+ "nested parent traversal is rejected" : {
223+ input : "foo/../../escape" ,
224+ expectError : true ,
225+ },
226+ }
227+ for name , tc := range tests {
228+ t .Run (name , func (t * testing.T ) {
229+ result , err := normalizeSubdir (tc .input )
230+ if tc .expectError {
231+ assert .Error (t , err )
232+ } else {
233+ assert .NoError (t , err )
234+ assert .Equal (t , tc .expected , result )
235+ }
236+ })
237+ }
238+ }
239+
240+ func TestCreateAppFromSubdir (t * testing.T ) {
241+ tests := map [string ]struct {
242+ setupTemplate func (t * testing.T ) string
243+ subdir string
244+ expectError bool
245+ errorContains string
246+ expectFiles []string
247+ }{
248+ "extracts subdirectory from local template" : {
249+ setupTemplate : func (t * testing.T ) string {
250+ tmpDir := t .TempDir ()
251+ // Create a subdirectory with a file
252+ subdir := filepath .Join (tmpDir , "apps" , "my-app" )
253+ require .NoError (t , os .MkdirAll (subdir , 0755 ))
254+ require .NoError (t , os .WriteFile (filepath .Join (subdir , "manifest.json" ), []byte (`{}` ), 0644 ))
255+ // Create a file at root that should NOT be copied
256+ require .NoError (t , os .WriteFile (filepath .Join (tmpDir , "README.md" ), []byte ("root readme" ), 0644 ))
257+ return tmpDir
258+ },
259+ subdir : "apps/my-app" ,
260+ expectFiles : []string {"manifest.json" },
261+ },
262+ "returns error for nonexistent subdirectory" : {
263+ setupTemplate : func (t * testing.T ) string {
264+ return t .TempDir ()
265+ },
266+ subdir : "nonexistent" ,
267+ expectError : true ,
268+ errorContains : "was not found in the template" ,
269+ },
270+ "returns error when subdir path is a file" : {
271+ setupTemplate : func (t * testing.T ) string {
272+ tmpDir := t .TempDir ()
273+ require .NoError (t , os .WriteFile (filepath .Join (tmpDir , "not-a-dir" ), []byte ("file" ), 0644 ))
274+ return tmpDir
275+ },
276+ subdir : "not-a-dir" ,
277+ expectError : true ,
278+ errorContains : "is not a directory" ,
279+ },
280+ }
281+ for name , tc := range tests {
282+ t .Run (name , func (t * testing.T ) {
283+ templateDir := tc .setupTemplate (t )
284+ outputDir := t .TempDir ()
285+ // Remove output dir so CopyDirectory can create it
286+ require .NoError (t , os .Remove (outputDir ))
287+
288+ template := Template {path : templateDir , isLocal : true }
289+ log := logger .New (func (event * logger.LogEvent ) {})
290+ fs := afero .NewOsFs ()
291+
292+ err := createAppFromSubdir (t .Context (), outputDir , template , "" , tc .subdir , log , fs )
293+
294+ if tc .expectError {
295+ assert .Error (t , err )
296+ if tc .errorContains != "" {
297+ assert .Contains (t , err .Error (), tc .errorContains )
298+ }
299+ } else {
300+ assert .NoError (t , err )
301+ for _ , f := range tc .expectFiles {
302+ _ , statErr := os .Stat (filepath .Join (outputDir , f ))
303+ assert .NoError (t , statErr , "expected file %s to exist" , f )
304+ }
305+ }
306+ })
307+ }
308+ }
309+
186310func Test_Create_installProjectDependencies (t * testing.T ) {
187311 tests := map [string ]struct {
188312 experiments []string
0 commit comments