diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec7a94dd..347caebc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -615,7 +615,7 @@ jobs: needs: preview if: github.event_name == 'pull_request' runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 5 steps: - name: Smoke test — app reachability run: | @@ -627,7 +627,19 @@ jobs: - name: Smoke test — API proxy rejects missing target run: | - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ needs.preview.outputs.preview-url }}/api/info") + # Pages Functions propagate a few seconds after "Deployment complete"; + # until they are live, /api/* falls through to the static 404 handler. + # Poll until the function answers (400) instead of failing on the first + # request, but still assert the final status is exactly 400. + STATUS="" + for i in $(seq 1 6); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ needs.preview.outputs.preview-url }}/api/info") + if [ "$STATUS" = "400" ]; then + break + fi + echo "Attempt $i: expected 400, got $STATUS (Functions may still be propagating); retrying in 10s..." + sleep 10 + done if [ "$STATUS" != "400" ]; then echo "Expected 400 for /api/info without x-target-url, got $STATUS" exit 1 @@ -827,7 +839,19 @@ jobs: - name: Smoke test — API proxy rejects missing target run: | - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ vars.STAGING_URL || needs.staging.outputs.deployment-url }}/api/info") + # Pages Functions propagate a few seconds after "Deployment complete"; + # until they are live, /api/* falls through to the static 404 handler. + # Poll until the function answers (400) instead of failing on the first + # request, but still assert the final status is exactly 400. + STATUS="" + for i in $(seq 1 6); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ vars.STAGING_URL || needs.staging.outputs.deployment-url }}/api/info") + if [ "$STATUS" = "400" ]; then + break + fi + echo "Attempt $i: expected 400, got $STATUS (Functions may still be propagating); retrying in 15s..." + sleep 15 + done if [ "$STATUS" != "400" ]; then echo "Expected 400 for /api/info without x-target-url, got $STATUS" exit 1 @@ -911,7 +935,7 @@ jobs: needs: deploy if: github.ref == 'refs/heads/main' && github.event_name == 'push' runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 5 steps: - name: Smoke test — app reachability run: | @@ -923,7 +947,19 @@ jobs: - name: Smoke test — API proxy rejects missing target run: | - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://app.comapeo.cloud/api/info") + # Pages Functions propagate a few seconds after "Deployment complete"; + # until they are live, /api/* falls through to the static 404 handler. + # Poll until the function answers (400) instead of failing on the first + # request, but still assert the final status is exactly 400. + STATUS="" + for i in $(seq 1 6); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://app.comapeo.cloud/api/info") + if [ "$STATUS" = "400" ]; then + break + fi + echo "Attempt $i: expected 400, got $STATUS (Functions may still be propagating); retrying in 10s..." + sleep 10 + done if [ "$STATUS" != "400" ]; then echo "Expected 400 for /api/info without x-target-url, got $STATUS" exit 1 diff --git a/tests/unit/screens/SettingsScreen.test.tsx b/tests/unit/screens/SettingsScreen.test.tsx index bc41ebcb..d28e0251 100644 --- a/tests/unit/screens/SettingsScreen.test.tsx +++ b/tests/unit/screens/SettingsScreen.test.tsx @@ -1,3 +1,4 @@ +import type { UserEvent } from '@testing-library/user-event'; import { server } from '@tests/mocks/node'; import { fireEvent, render, screen, userEvent } from '@tests/mocks/test-utils'; import { HttpResponse, http } from 'msw'; @@ -26,6 +27,15 @@ vi.mock('@/lib/local-storage-utils', () => ({ clearAllStorage: vi.fn(() => Promise.resolve()), })); +async function generateInvite(user: UserEvent) { + await user.type( + screen.getByLabelText('Remote Archive URL'), + 'https://archive.example.com', + ); + await user.type(screen.getByLabelText('Bearer Token'), 'my-secret-token'); + await user.click(screen.getByRole('button', { name: 'Generate Invite' })); +} + beforeEach(async () => { await resetDb(); useAuthStore.setState({ @@ -82,12 +92,7 @@ describe('SettingsScreen', () => { const user = userEvent.setup(); render(); - await user.type( - screen.getByLabelText('Remote Archive URL'), - 'https://archive.example.com', - ); - await user.type(screen.getByLabelText('Bearer Token'), 'my-secret-token'); - await user.click(screen.getByRole('button', { name: 'Generate Invite' })); + await generateInvite(user); // Results header should appear expect(await screen.findByText('Results')).toBeInTheDocument(); @@ -113,12 +118,7 @@ describe('SettingsScreen', () => { const user = userEvent.setup(); render(); - await user.type( - screen.getByLabelText('Remote Archive URL'), - 'https://archive.example.com', - ); - await user.type(screen.getByLabelText('Bearer Token'), 'my-secret-token'); - await user.click(screen.getByRole('button', { name: 'Generate Invite' })); + await generateInvite(user); expect(await screen.findByText('Expires in 24 hours.')).toBeInTheDocument(); }); @@ -141,12 +141,7 @@ describe('SettingsScreen', () => { const user = userEvent.setup(); render(); - await user.type( - screen.getByLabelText('Remote Archive URL'), - 'https://archive.example.com', - ); - await user.type(screen.getByLabelText('Bearer Token'), 'my-secret-token'); - await user.click(screen.getByRole('button', { name: 'Generate Invite' })); + await generateInvite(user); expect( await screen.findByText( @@ -163,13 +158,7 @@ describe('SettingsScreen', () => { const user = userEvent.setup(); render(); - // Fill form and generate - await user.type( - screen.getByLabelText('Remote Archive URL'), - 'https://archive.example.com', - ); - await user.type(screen.getByLabelText('Bearer Token'), 'my-secret-token'); - await user.click(screen.getByRole('button', { name: 'Generate Invite' })); + await generateInvite(user); // Wait for results to render await screen.findByText('Results');