File size: 4,100 Bytes
79ed62f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { http, HttpResponse } from 'msw';
import { useTripStore } from '../../../src/store/tripStore';
import { filesApi } from '../../../src/api/client';
import { resetAllStores, seedStore } from '../../helpers/store';
import { buildTripFile } from '../../helpers/factories';
import { server } from '../../helpers/msw/server';

vi.mock('../../../src/api/websocket', () => ({
  connect: vi.fn(),
  disconnect: vi.fn(),
  getSocketId: vi.fn(() => null),
  joinTrip: vi.fn(),
  leaveTrip: vi.fn(),
  addListener: vi.fn(),
  removeListener: vi.fn(),
  setRefetchCallback: vi.fn(),
  setPreReconnectHook: vi.fn(),
}));

beforeEach(() => {
  resetAllStores();
});

describe('filesSlice', () => {
  describe('loadFiles', () => {
    it('FE-FILES-001: loadFiles fetches and replaces files array', async () => {
      const staleFile = buildTripFile({ trip_id: 1, filename: 'stale.pdf' });
      seedStore(useTripStore, { files: [staleFile] });

      const freshFile = buildTripFile({ trip_id: 1, filename: 'fresh.pdf' });
      server.use(
        http.get('/api/trips/1/files', () => HttpResponse.json({ files: [freshFile] })),
      );

      await useTripStore.getState().loadFiles(1);

      const files = useTripStore.getState().files;
      expect(files).toHaveLength(1);
      expect(files[0].filename).toBe('fresh.pdf');
    });

    it('FE-FILES-002: loadFiles silently catches errors', async () => {
      server.use(
        http.get('/api/trips/1/files', () =>
          HttpResponse.json({ message: 'Error' }, { status: 500 })
        ),
      );

      // Should not throw
      await useTripStore.getState().loadFiles(1);
    });
  });

  describe('addFile', () => {
    it('FE-FILES-003: addFile uploads and prepends file to files array', async () => {
      const existing = buildTripFile({ trip_id: 1, filename: 'existing.pdf' });
      seedStore(useTripStore, { files: [existing] });

      const uploaded = buildTripFile({ trip_id: 1, filename: 'new-upload.pdf' });
      // FormData POST hangs on CI — mock at the API boundary instead of MSW.
      const uploadSpy = vi.spyOn(filesApi, 'upload').mockResolvedValueOnce({ file: uploaded });

      const formData = new FormData();
      formData.append('file', new Blob(['content'], { type: 'application/pdf' }), 'new-upload.pdf');

      const result = await useTripStore.getState().addFile(1, formData);
      uploadSpy.mockRestore();

      expect(result.filename).toBe('new-upload.pdf');
      const files = useTripStore.getState().files;
      expect(files).toHaveLength(2);
      // prepends
      expect(files[0].filename).toBe('new-upload.pdf');
    });

    it('FE-FILES-004: addFile on failure throws', async () => {
      server.use(
        http.post('/api/trips/1/files', () =>
          HttpResponse.json({ message: 'Error' }, { status: 500 })
        ),
      );

      const formData = new FormData();

      await expect(useTripStore.getState().addFile(1, formData)).rejects.toThrow();
    });
  });

  describe('deleteFile', () => {
    it('FE-FILES-005: deleteFile removes file from array after API success', async () => {
      const file1 = buildTripFile({ id: 10, trip_id: 1 });
      const file2 = buildTripFile({ id: 20, trip_id: 1 });
      seedStore(useTripStore, { files: [file1, file2] });

      await useTripStore.getState().deleteFile(1, 10);

      const files = useTripStore.getState().files;
      expect(files).toHaveLength(1);
      expect(files[0].id).toBe(20);
    });

    it('FE-FILES-006: deleteFile on failure throws', async () => {
      const file = buildTripFile({ id: 10, trip_id: 1 });
      seedStore(useTripStore, { files: [file] });

      server.use(
        http.delete('/api/trips/1/files/10', () =>
          HttpResponse.json({ message: 'Error' }, { status: 500 })
        ),
      );

      await expect(useTripStore.getState().deleteFile(1, 10)).rejects.toThrow();

      // File remains since server-first (only removes after success)
      expect(useTripStore.getState().files).toHaveLength(1);
    });
  });
});