| 402 | } |
| 403 | |
| 404 | func TestWriteFile_ReportsIOError(t *testing.T) { |
| 405 | t.Parallel() |
| 406 | |
| 407 | logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) |
| 408 | fs := afero.NewMemMapFs() |
| 409 | api := agentfiles.NewAPI(logger, fs, nil) |
| 410 | |
| 411 | tmpdir := os.TempDir() |
| 412 | path := filepath.Join(tmpdir, "write-io-error") |
| 413 | err := afero.WriteFile(fs, path, []byte("original"), 0o644) |
| 414 | require.NoError(t, err) |
| 415 | |
| 416 | ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) |
| 417 | defer cancel() |
| 418 | |
| 419 | // A reader that always errors simulates a failed body read |
| 420 | // (e.g. network interruption). The atomic write should leave |
| 421 | // the original file intact. |
| 422 | body := iotest.ErrReader(xerrors.New("simulated I/O error")) |
| 423 | w := httptest.NewRecorder() |
| 424 | r := httptest.NewRequestWithContext(ctx, http.MethodPost, |
| 425 | fmt.Sprintf("/write-file?path=%s", path), body) |
| 426 | api.Routes().ServeHTTP(w, r) |
| 427 | |
| 428 | require.Equal(t, http.StatusInternalServerError, w.Code) |
| 429 | got := &codersdk.Error{} |
| 430 | err = json.NewDecoder(w.Body).Decode(got) |
| 431 | require.NoError(t, err) |
| 432 | require.ErrorContains(t, got, "simulated I/O error") |
| 433 | |
| 434 | // The original file must survive the failed write. |
| 435 | data, err := afero.ReadFile(fs, path) |
| 436 | require.NoError(t, err) |
| 437 | require.Equal(t, "original", string(data)) |
| 438 | } |
| 439 | |
| 440 | func TestWriteFile_PreservesPermissions(t *testing.T) { |
| 441 | t.Parallel() |