@@ -1307,3 +1307,368 @@ func TestAuthenticationErrorsIntegration(t *testing.T) {
13071307 })
13081308 }
13091309}
1310+
1311+ // TestServiceForkIntegration tests forking a service with --now strategy and validates data is correctly copied
1312+ func TestServiceForkIntegration (t * testing.T ) {
1313+ config .SetTestServiceName (t )
1314+ // Check for required environment variables
1315+ publicKey := os .Getenv ("TIGER_PUBLIC_KEY_INTEGRATION" )
1316+ secretKey := os .Getenv ("TIGER_SECRET_KEY_INTEGRATION" )
1317+ projectID := os .Getenv ("TIGER_PROJECT_ID_INTEGRATION" )
1318+ if publicKey == "" || secretKey == "" || projectID == "" {
1319+ t .Skip ("Skipping integration test: TIGER_PUBLIC_KEY_INTEGRATION, TIGER_SECRET_KEY_INTEGRATION, and TIGER_PROJECT_ID_INTEGRATION must be set" )
1320+ }
1321+
1322+ // Set up isolated test environment with temporary config directory
1323+ tmpDir := setupIntegrationTest (t )
1324+ t .Logf ("Using temporary config directory: %s" , tmpDir )
1325+
1326+ // Generate unique names to avoid conflicts
1327+ timestamp := time .Now ().Unix ()
1328+ sourceServiceName := fmt .Sprintf ("integration-fork-source-%d" , timestamp )
1329+ tableName := fmt .Sprintf ("fork_test_data_%d" , timestamp )
1330+
1331+ var sourceServiceID string
1332+ var forkedServiceID string
1333+
1334+ // Always logout at the end to clean up credentials
1335+ defer func () {
1336+ t .Logf ("Cleaning up authentication" )
1337+ _ , err := executeIntegrationCommand (t .Context (), "auth" , "logout" )
1338+ if err != nil {
1339+ t .Logf ("Warning: Failed to logout: %v" , err )
1340+ }
1341+ }()
1342+
1343+ // Cleanup function to ensure source service is deleted
1344+ defer func () {
1345+ if sourceServiceID != "" {
1346+ t .Logf ("Cleaning up source service: %s" , sourceServiceID )
1347+ _ , err := executeIntegrationCommand (
1348+ t .Context (),
1349+ "service" , "delete" , sourceServiceID ,
1350+ "--confirm" ,
1351+ "--wait-timeout" , "5m" ,
1352+ )
1353+ if err != nil {
1354+ t .Logf ("Warning: Failed to cleanup source service %s: %v" , sourceServiceID , err )
1355+ }
1356+ }
1357+ }()
1358+
1359+ // Cleanup function to ensure forked service is deleted
1360+ defer func () {
1361+ if forkedServiceID != "" {
1362+ t .Logf ("Cleaning up forked service: %s" , forkedServiceID )
1363+ _ , err := executeIntegrationCommand (
1364+ t .Context (),
1365+ "service" , "delete" , forkedServiceID ,
1366+ "--confirm" ,
1367+ "--wait-timeout" , "5m" ,
1368+ )
1369+ if err != nil {
1370+ t .Logf ("Warning: Failed to cleanup forked service %s: %v" , forkedServiceID , err )
1371+ }
1372+ }
1373+ }()
1374+
1375+ t .Run ("Login" , func (t * testing.T ) {
1376+ t .Logf ("Logging in with public key: %s" , publicKey [:8 ]+ "..." ) // Only show first 8 chars
1377+
1378+ output , err := executeIntegrationCommand (
1379+ t .Context (),
1380+ "auth" , "login" ,
1381+ "--public-key" , publicKey ,
1382+ "--secret-key" , secretKey ,
1383+ "--project-id" , projectID ,
1384+ )
1385+
1386+ if err != nil {
1387+ t .Fatalf ("Login failed: %v\n Output: %s" , err , output )
1388+ }
1389+
1390+ t .Logf ("Login successful" )
1391+ })
1392+
1393+ t .Run ("CreateSourceService" , func (t * testing.T ) {
1394+ t .Logf ("Creating source service: %s" , sourceServiceName )
1395+
1396+ output , err := executeIntegrationCommand (
1397+ t .Context (),
1398+ "service" , "create" ,
1399+ "--name" , sourceServiceName ,
1400+ "--cpu" , "shared" ,
1401+ "--wait-timeout" , "15m" ,
1402+ "--no-set-default" ,
1403+ "--output" , "json" ,
1404+ )
1405+
1406+ if err != nil {
1407+ t .Fatalf ("Source service creation failed: %v\n Output: %s" , err , output )
1408+ }
1409+
1410+ extractedServiceID := extractServiceIDFromCreateOutput (t , output )
1411+ if extractedServiceID == "" {
1412+ t .Fatalf ("Could not extract source service ID from create output: %s" , output )
1413+ }
1414+
1415+ sourceServiceID = extractedServiceID
1416+ t .Logf ("Created source service with ID: %s" , sourceServiceID )
1417+ })
1418+
1419+ t .Run ("InsertTestData" , func (t * testing.T ) {
1420+ if sourceServiceID == "" {
1421+ t .Skip ("No source service ID available" )
1422+ }
1423+
1424+ t .Logf ("Creating test table: %s" , tableName )
1425+
1426+ // Create table
1427+ output , err := executeIntegrationCommand (
1428+ t .Context (),
1429+ "db" , "psql" , sourceServiceID ,
1430+ "--" , "-c" , fmt .Sprintf ("CREATE TABLE %s (id INT PRIMARY KEY, data TEXT, created_at TIMESTAMP DEFAULT NOW());" , tableName ),
1431+ )
1432+
1433+ if err != nil {
1434+ t .Fatalf ("Failed to create test table: %v\n Output: %s" , err , output )
1435+ }
1436+
1437+ t .Logf ("Inserting test data into table: %s" , tableName )
1438+
1439+ // Insert test data
1440+ output , err = executeIntegrationCommand (
1441+ t .Context (),
1442+ "db" , "psql" , sourceServiceID ,
1443+ "--" , "-c" , fmt .Sprintf ("INSERT INTO %s (id, data) VALUES (1, 'test-row-1'), (2, 'test-row-2'), (3, 'test-row-3');" , tableName ),
1444+ )
1445+
1446+ if err != nil {
1447+ t .Fatalf ("Failed to insert test data: %v\n Output: %s" , err , output )
1448+ }
1449+
1450+ t .Logf ("✅ Test data inserted successfully" )
1451+ })
1452+
1453+ t .Run ("VerifySourceData" , func (t * testing.T ) {
1454+ if sourceServiceID == "" {
1455+ t .Skip ("No source service ID available" )
1456+ }
1457+
1458+ t .Logf ("Verifying test data in source service" )
1459+
1460+ output , err := executeIntegrationCommand (
1461+ t .Context (),
1462+ "db" , "psql" , sourceServiceID ,
1463+ "--" , "-c" , fmt .Sprintf ("SELECT * FROM %s ORDER BY id;" , tableName ),
1464+ )
1465+
1466+ if err != nil {
1467+ t .Fatalf ("Failed to query test data: %v\n Output: %s" , err , output )
1468+ }
1469+
1470+ // Verify all three rows are present
1471+ if ! strings .Contains (output , "test-row-1" ) {
1472+ t .Errorf ("Expected 'test-row-1' in output, got: %s" , output )
1473+ }
1474+ if ! strings .Contains (output , "test-row-2" ) {
1475+ t .Errorf ("Expected 'test-row-2' in output, got: %s" , output )
1476+ }
1477+ if ! strings .Contains (output , "test-row-3" ) {
1478+ t .Errorf ("Expected 'test-row-3' in output, got: %s" , output )
1479+ }
1480+
1481+ t .Logf ("✅ Source data verified: 3 rows present" )
1482+ })
1483+
1484+ t .Run ("ForkService" , func (t * testing.T ) {
1485+ if sourceServiceID == "" {
1486+ t .Skip ("No source service ID available" )
1487+ }
1488+
1489+ t .Logf ("Forking service: %s with --now strategy" , sourceServiceID )
1490+
1491+ output , err := executeIntegrationCommand (
1492+ t .Context (),
1493+ "service" , "fork" , sourceServiceID ,
1494+ "--now" ,
1495+ "--wait-timeout" , "15m" ,
1496+ "--no-set-default" ,
1497+ "--output" , "json" ,
1498+ )
1499+
1500+ if err != nil {
1501+ t .Fatalf ("Service fork failed: %v\n Output: %s" , err , output )
1502+ }
1503+
1504+ extractedServiceID := extractServiceIDFromCreateOutput (t , output )
1505+ if extractedServiceID == "" {
1506+ t .Fatalf ("Could not extract forked service ID from fork output: %s" , output )
1507+ }
1508+
1509+ forkedServiceID = extractedServiceID
1510+ t .Logf ("✅ Created forked service with ID: %s" , forkedServiceID )
1511+ })
1512+
1513+ t .Run ("VerifyForkedData" , func (t * testing.T ) {
1514+ if forkedServiceID == "" {
1515+ t .Skip ("No forked service ID available" )
1516+ }
1517+
1518+ t .Logf ("Verifying test data in forked service" )
1519+
1520+ output , err := executeIntegrationCommand (
1521+ t .Context (),
1522+ "db" , "psql" , forkedServiceID ,
1523+ "--" , "-c" , fmt .Sprintf ("SELECT * FROM %s ORDER BY id;" , tableName ),
1524+ )
1525+
1526+ if err != nil {
1527+ t .Fatalf ("Failed to query forked service data: %v\n Output: %s" , err , output )
1528+ }
1529+
1530+ // Verify all three rows are present in fork
1531+ if ! strings .Contains (output , "test-row-1" ) {
1532+ t .Errorf ("Expected 'test-row-1' in forked service output, got: %s" , output )
1533+ }
1534+ if ! strings .Contains (output , "test-row-2" ) {
1535+ t .Errorf ("Expected 'test-row-2' in forked service output, got: %s" , output )
1536+ }
1537+ if ! strings .Contains (output , "test-row-3" ) {
1538+ t .Errorf ("Expected 'test-row-3' in forked service output, got: %s" , output )
1539+ }
1540+
1541+ t .Logf ("✅ Forked data verified: 3 rows present matching source" )
1542+ })
1543+
1544+ t .Run ("VerifyDataIndependence" , func (t * testing.T ) {
1545+ if sourceServiceID == "" || forkedServiceID == "" {
1546+ t .Skip ("Source or forked service ID not available" )
1547+ }
1548+
1549+ t .Logf ("Verifying data independence between source and fork" )
1550+
1551+ // Insert new data in forked service
1552+ t .Logf ("Inserting new row in forked service" )
1553+ output , err := executeIntegrationCommand (
1554+ t .Context (),
1555+ "db" , "psql" , forkedServiceID ,
1556+ "--" , "-c" , fmt .Sprintf ("INSERT INTO %s (id, data) VALUES (4, 'fork-only-row');" , tableName ),
1557+ )
1558+
1559+ if err != nil {
1560+ t .Fatalf ("Failed to insert data in fork: %v\n Output: %s" , err , output )
1561+ }
1562+
1563+ // Verify fork has 4 rows
1564+ t .Logf ("Verifying fork has 4 rows" )
1565+ output , err = executeIntegrationCommand (
1566+ t .Context (),
1567+ "db" , "psql" , forkedServiceID ,
1568+ "--" , "-c" , fmt .Sprintf ("SELECT COUNT(*) FROM %s;" , tableName ),
1569+ )
1570+
1571+ if err != nil {
1572+ t .Fatalf ("Failed to count rows in fork: %v\n Output: %s" , err , output )
1573+ }
1574+
1575+ if ! strings .Contains (output , "4" ) {
1576+ t .Errorf ("Expected 4 rows in fork after insert, got: %s" , output )
1577+ }
1578+
1579+ // Verify source still has 3 rows (unchanged)
1580+ t .Logf ("Verifying source still has 3 rows (unchanged)" )
1581+ output , err = executeIntegrationCommand (
1582+ t .Context (),
1583+ "db" , "psql" , sourceServiceID ,
1584+ "--" , "-c" , fmt .Sprintf ("SELECT COUNT(*) FROM %s;" , tableName ),
1585+ )
1586+
1587+ if err != nil {
1588+ t .Fatalf ("Failed to count rows in source: %v\n Output: %s" , err , output )
1589+ }
1590+
1591+ if ! strings .Contains (output , "3" ) {
1592+ t .Errorf ("Expected 3 rows in source (unchanged), got: %s" , output )
1593+ }
1594+
1595+ // Verify source doesn't have fork-only row
1596+ output , err = executeIntegrationCommand (
1597+ t .Context (),
1598+ "db" , "psql" , sourceServiceID ,
1599+ "--" , "-c" , fmt .Sprintf ("SELECT * FROM %s WHERE data = 'fork-only-row';" , tableName ),
1600+ )
1601+
1602+ if err != nil {
1603+ t .Fatalf ("Failed to query source for fork-only row: %v\n Output: %s" , err , output )
1604+ }
1605+
1606+ // The output should show "0 rows" or similar since the row shouldn't exist
1607+ if strings .Contains (output , "fork-only-row" ) {
1608+ t .Errorf ("Source service should not contain fork-only row, but got: %s" , output )
1609+ }
1610+
1611+ t .Logf ("✅ Data independence verified: fork and source are truly independent" )
1612+ })
1613+
1614+ t .Run ("DeleteSourceService" , func (t * testing.T ) {
1615+ if sourceServiceID == "" {
1616+ t .Skip ("No source service ID available" )
1617+ }
1618+
1619+ t .Logf ("Deleting source service: %s" , sourceServiceID )
1620+
1621+ output , err := executeIntegrationCommand (
1622+ t .Context (),
1623+ "service" , "delete" , sourceServiceID ,
1624+ "--confirm" ,
1625+ "--wait-timeout" , "10m" ,
1626+ )
1627+
1628+ if err != nil {
1629+ t .Fatalf ("Source service deletion failed: %v\n Output: %s" , err , output )
1630+ }
1631+
1632+ // Clear sourceServiceID so cleanup doesn't try to delete again
1633+ sourceServiceID = ""
1634+ t .Logf ("✅ Source service deleted successfully" )
1635+ })
1636+
1637+ t .Run ("DeleteForkedService" , func (t * testing.T ) {
1638+ if forkedServiceID == "" {
1639+ t .Skip ("No forked service ID available" )
1640+ }
1641+
1642+ t .Logf ("Deleting forked service: %s" , forkedServiceID )
1643+
1644+ output , err := executeIntegrationCommand (
1645+ t .Context (),
1646+ "service" , "delete" , forkedServiceID ,
1647+ "--confirm" ,
1648+ "--wait-timeout" , "10m" ,
1649+ )
1650+
1651+ if err != nil {
1652+ t .Fatalf ("Forked service deletion failed: %v\n Output: %s" , err , output )
1653+ }
1654+
1655+ // Clear forkedServiceID so cleanup doesn't try to delete again
1656+ forkedServiceID = ""
1657+ t .Logf ("✅ Forked service deleted successfully" )
1658+ })
1659+
1660+ t .Run ("Logout" , func (t * testing.T ) {
1661+ t .Logf ("Logging out" )
1662+
1663+ output , err := executeIntegrationCommand (
1664+ t .Context (),
1665+ "auth" , "logout" ,
1666+ )
1667+
1668+ if err != nil {
1669+ t .Fatalf ("Logout failed: %v\n Output: %s" , err , output )
1670+ }
1671+
1672+ t .Logf ("Logout successful" )
1673+ })
1674+ }
0 commit comments