@@ -1216,6 +1216,262 @@ func TestCreateCommand_WithPGMajorVersion(t *testing.T) {
12161216 }
12171217}
12181218
1219+ // Test CreateAttachment functionality
1220+ func TestCreateAttachment (t * testing.T ) {
1221+ ctx := setupTestContext ()
1222+
1223+ clusterID := "test-cluster-123"
1224+
1225+ t .Run ("successful attachment creation" , func (t * testing.T ) {
1226+ mockUiex := & mock.UiexClient {
1227+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1228+ assert .Equal (t , clusterID , clusterId )
1229+ assert .Equal (t , "test-app" , input .AppName )
1230+ return uiex.CreateAttachmentResponse {
1231+ Data : struct {
1232+ Id int64 `json:"id"`
1233+ AppId int64 `json:"app_id"`
1234+ ManagedServiceId int64 `json:"managed_service_id"`
1235+ AttachedAt string `json:"attached_at"`
1236+ }{
1237+ Id : 1 ,
1238+ AppId : 100 ,
1239+ ManagedServiceId : 200 ,
1240+ AttachedAt : "2025-01-15T10:00:00Z" ,
1241+ },
1242+ }, nil
1243+ },
1244+ }
1245+
1246+ ctx := uiexutil .NewContextWithClient (ctx , mockUiex )
1247+
1248+ response , err := mockUiex .CreateAttachment (ctx , clusterID , uiex.CreateAttachmentInput {
1249+ AppName : "test-app" ,
1250+ })
1251+
1252+ require .NoError (t , err )
1253+ assert .Equal (t , int64 (1 ), response .Data .Id )
1254+ assert .Equal (t , int64 (100 ), response .Data .AppId )
1255+ assert .Equal (t , int64 (200 ), response .Data .ManagedServiceId )
1256+ assert .Equal (t , "2025-01-15T10:00:00Z" , response .Data .AttachedAt )
1257+ })
1258+
1259+ t .Run ("idempotent - returns existing attachment" , func (t * testing.T ) {
1260+ mockUiex := & mock.UiexClient {
1261+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1262+ // Simulating the idempotent case where attachment already exists
1263+ return uiex.CreateAttachmentResponse {
1264+ Data : struct {
1265+ Id int64 `json:"id"`
1266+ AppId int64 `json:"app_id"`
1267+ ManagedServiceId int64 `json:"managed_service_id"`
1268+ AttachedAt string `json:"attached_at"`
1269+ }{
1270+ Id : 42 , // Existing attachment ID
1271+ AppId : 100 ,
1272+ ManagedServiceId : 200 ,
1273+ AttachedAt : "2025-01-14T09:00:00Z" , // Earlier timestamp
1274+ },
1275+ }, nil
1276+ },
1277+ }
1278+
1279+ ctx := uiexutil .NewContextWithClient (ctx , mockUiex )
1280+
1281+ response , err := mockUiex .CreateAttachment (ctx , clusterID , uiex.CreateAttachmentInput {
1282+ AppName : "already-attached-app" ,
1283+ })
1284+
1285+ require .NoError (t , err )
1286+ assert .Equal (t , int64 (42 ), response .Data .Id )
1287+ })
1288+
1289+ t .Run ("error - cluster not found" , func (t * testing.T ) {
1290+ mockUiex := & mock.UiexClient {
1291+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1292+ return uiex.CreateAttachmentResponse {}, fmt .Errorf ("cluster %s not found" , clusterId )
1293+ },
1294+ }
1295+
1296+ ctx := uiexutil .NewContextWithClient (ctx , mockUiex )
1297+
1298+ _ , err := mockUiex .CreateAttachment (ctx , "nonexistent-cluster" , uiex.CreateAttachmentInput {
1299+ AppName : "test-app" ,
1300+ })
1301+
1302+ assert .Error (t , err )
1303+ assert .Contains (t , err .Error (), "not found" )
1304+ })
1305+
1306+ t .Run ("error - access denied" , func (t * testing.T ) {
1307+ mockUiex := & mock.UiexClient {
1308+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1309+ return uiex.CreateAttachmentResponse {}, fmt .Errorf ("access denied: you don't have permission to attach cluster %s" , clusterId )
1310+ },
1311+ }
1312+
1313+ ctx := uiexutil .NewContextWithClient (ctx , mockUiex )
1314+
1315+ _ , err := mockUiex .CreateAttachment (ctx , clusterID , uiex.CreateAttachmentInput {
1316+ AppName : "test-app" ,
1317+ })
1318+
1319+ assert .Error (t , err )
1320+ assert .Contains (t , err .Error (), "access denied" )
1321+ })
1322+
1323+ t .Run ("error - app not found" , func (t * testing.T ) {
1324+ mockUiex := & mock.UiexClient {
1325+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1326+ return uiex.CreateAttachmentResponse {}, fmt .Errorf ("app not found" )
1327+ },
1328+ }
1329+
1330+ ctx := uiexutil .NewContextWithClient (ctx , mockUiex )
1331+
1332+ _ , err := mockUiex .CreateAttachment (ctx , clusterID , uiex.CreateAttachmentInput {
1333+ AppName : "nonexistent-app" ,
1334+ })
1335+
1336+ assert .Error (t , err )
1337+ assert .Contains (t , err .Error (), "not found" )
1338+ })
1339+ }
1340+
1341+ // Test attach command integration with CreateAttachment
1342+ func TestAttachCommand_CreatesAttachment (t * testing.T ) {
1343+ ctx := setupTestContext ()
1344+
1345+ clusterID := "test-cluster-123"
1346+ appName := "test-app"
1347+
1348+ expectedCluster := uiex.ManagedCluster {
1349+ Id : clusterID ,
1350+ Name : "test-cluster" ,
1351+ Region : "ord" ,
1352+ Status : "ready" ,
1353+ Organization : fly.Organization {
1354+ Slug : "test-org" ,
1355+ },
1356+ }
1357+
1358+ connectionURI := "postgresql://user:pass@host:5432/db"
1359+
1360+ // Track whether CreateAttachment was called
1361+ createAttachmentCalled := false
1362+ var capturedAppName string
1363+
1364+ mockUiex := & mock.UiexClient {
1365+ GetManagedClusterByIdFunc : func (ctx context.Context , id string ) (uiex.GetManagedClusterResponse , error ) {
1366+ assert .Equal (t , clusterID , id )
1367+ return uiex.GetManagedClusterResponse {
1368+ Data : expectedCluster ,
1369+ Credentials : uiex.GetManagedClusterCredentialsResponse {
1370+ ConnectionUri : connectionURI ,
1371+ User : "fly-user" ,
1372+ Password : "test-password" ,
1373+ DBName : "fly_db" ,
1374+ },
1375+ }, nil
1376+ },
1377+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1378+ createAttachmentCalled = true
1379+ capturedAppName = input .AppName
1380+ assert .Equal (t , clusterID , clusterId )
1381+ return uiex.CreateAttachmentResponse {
1382+ Data : struct {
1383+ Id int64 `json:"id"`
1384+ AppId int64 `json:"app_id"`
1385+ ManagedServiceId int64 `json:"managed_service_id"`
1386+ AttachedAt string `json:"attached_at"`
1387+ }{
1388+ Id : 1 ,
1389+ AppId : 100 ,
1390+ ManagedServiceId : 200 ,
1391+ AttachedAt : "2025-01-15T10:00:00Z" ,
1392+ },
1393+ }, nil
1394+ },
1395+ }
1396+
1397+ ctx = uiexutil .NewContextWithClient (ctx , mockUiex )
1398+
1399+ // Simulate the attach command flow: get cluster, then create attachment
1400+ response , err := mockUiex .GetManagedClusterById (ctx , clusterID )
1401+ require .NoError (t , err )
1402+ assert .Equal (t , expectedCluster .Id , response .Data .Id )
1403+
1404+ // Create attachment (this simulates what runAttach does after setting secrets)
1405+ attachInput := uiex.CreateAttachmentInput {
1406+ AppName : appName ,
1407+ }
1408+ _ , err = mockUiex .CreateAttachment (ctx , clusterID , attachInput )
1409+ require .NoError (t , err )
1410+
1411+ // Verify CreateAttachment was called with correct app name
1412+ assert .True (t , createAttachmentCalled , "CreateAttachment should be called during attach" )
1413+ assert .Equal (t , appName , capturedAppName , "App name should be passed to CreateAttachment" )
1414+ }
1415+
1416+ // Test that attach command handles CreateAttachment errors gracefully
1417+ func TestAttachCommand_HandlesAttachmentErrorGracefully (t * testing.T ) {
1418+ ctx := setupTestContext ()
1419+
1420+ clusterID := "test-cluster-123"
1421+ appName := "test-app"
1422+
1423+ expectedCluster := uiex.ManagedCluster {
1424+ Id : clusterID ,
1425+ Name : "test-cluster" ,
1426+ Region : "ord" ,
1427+ Status : "ready" ,
1428+ Organization : fly.Organization {
1429+ Slug : "test-org" ,
1430+ },
1431+ }
1432+
1433+ connectionURI := "postgresql://user:pass@host:5432/db"
1434+
1435+ mockUiex := & mock.UiexClient {
1436+ GetManagedClusterByIdFunc : func (ctx context.Context , id string ) (uiex.GetManagedClusterResponse , error ) {
1437+ return uiex.GetManagedClusterResponse {
1438+ Data : expectedCluster ,
1439+ Credentials : uiex.GetManagedClusterCredentialsResponse {
1440+ ConnectionUri : connectionURI ,
1441+ User : "fly-user" ,
1442+ Password : "test-password" ,
1443+ DBName : "fly_db" ,
1444+ },
1445+ }, nil
1446+ },
1447+ CreateAttachmentFunc : func (ctx context.Context , clusterId string , input uiex.CreateAttachmentInput ) (uiex.CreateAttachmentResponse , error ) {
1448+ // Simulate a failure in creating attachment
1449+ return uiex.CreateAttachmentResponse {}, fmt .Errorf ("failed to create attachment" )
1450+ },
1451+ }
1452+
1453+ ctx = uiexutil .NewContextWithClient (ctx , mockUiex )
1454+
1455+ // Get cluster - should succeed
1456+ response , err := mockUiex .GetManagedClusterById (ctx , clusterID )
1457+ require .NoError (t , err )
1458+ assert .Equal (t , expectedCluster .Id , response .Data .Id )
1459+
1460+ // Create attachment - should fail but we handle it gracefully
1461+ attachInput := uiex.CreateAttachmentInput {
1462+ AppName : appName ,
1463+ }
1464+ _ , err = mockUiex .CreateAttachment (ctx , clusterID , attachInput )
1465+
1466+ // The error exists but in runAttach we just log a warning
1467+ assert .Error (t , err )
1468+ assert .Contains (t , err .Error (), "failed to create attachment" )
1469+
1470+ // In the actual implementation, this is handled as a warning:
1471+ // fmt.Fprintf(io.ErrOut, "Warning: failed to create attachment record: %v\n", err)
1472+ // The attach command still succeeds because the secret was set
1473+ }
1474+
12191475// Test invalid PG major version error message
12201476func TestInvalidPGMajorVersion_Error (t * testing.T ) {
12211477 invalidVersions := []int {15 , 18 , 14 , 13 , 19 , 0 , - 1 }
0 commit comments