| 1485 | } |
| 1486 | |
| 1487 | func TestWriteFile_FollowsSymlinks(t *testing.T) { |
| 1488 | t.Parallel() |
| 1489 | |
| 1490 | if runtime.GOOS == "windows" { |
| 1491 | t.Skip("symlinks are not reliably supported on Windows") |
| 1492 | } |
| 1493 | |
| 1494 | dir := t.TempDir() |
| 1495 | logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) |
| 1496 | osFs := afero.NewOsFs() |
| 1497 | api := agentfiles.NewAPI(logger, osFs, nil) |
| 1498 | |
| 1499 | // Create a real file and a symlink pointing to it. |
| 1500 | realPath := filepath.Join(dir, "real.txt") |
| 1501 | err := afero.WriteFile(osFs, realPath, []byte("original"), 0o644) |
| 1502 | require.NoError(t, err) |
| 1503 | |
| 1504 | linkPath := filepath.Join(dir, "link.txt") |
| 1505 | err = os.Symlink(realPath, linkPath) |
| 1506 | require.NoError(t, err) |
| 1507 | |
| 1508 | ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) |
| 1509 | defer cancel() |
| 1510 | |
| 1511 | // Write through the symlink. |
| 1512 | w := httptest.NewRecorder() |
| 1513 | r := httptest.NewRequestWithContext(ctx, http.MethodPost, |
| 1514 | fmt.Sprintf("/write-file?path=%s", linkPath), |
| 1515 | bytes.NewReader([]byte("updated"))) |
| 1516 | api.Routes().ServeHTTP(w, r) |
| 1517 | require.Equal(t, http.StatusOK, w.Code) |
| 1518 | |
| 1519 | // The symlink must still be a symlink. |
| 1520 | fi, err := os.Lstat(linkPath) |
| 1521 | require.NoError(t, err) |
| 1522 | require.NotZero(t, fi.Mode()&os.ModeSymlink, "symlink was replaced") |
| 1523 | |
| 1524 | // The real file must have the new content. |
| 1525 | data, err := os.ReadFile(realPath) |
| 1526 | require.NoError(t, err) |
| 1527 | require.Equal(t, "updated", string(data)) |
| 1528 | } |
| 1529 | |
| 1530 | func TestEditFiles_FollowsSymlinks(t *testing.T) { |
| 1531 | t.Parallel() |