3 Commits

Author SHA1 Message Date
david b162750a67 fix: remove permissions module mock from tournament-update tests to prevent test pollution
Pull Request / unit-tests (pull_request) Failing after 55s
Pull Request / build-and-deploy-ci (pull_request) Has been skipped
Pull Request / analyze-bump-type (pull_request) Has been skipped
tournament-update.test.ts was mocking the entire @/lib/permissions module,
which replaced canManageTournament globally and caused permissions.test.ts
to fail when run in the same process.

Instead of mocking permissions, mock its dependencies (auth-simple and prisma)
so canManageTournament runs through naturally.
2026-05-16 20:03:10 -07:00
david 671ee78a47 fix: mount /apps and docker socket in PR workflow build-and-deploy-ci job
Pull Request / unit-tests (pull_request) Failing after 55s
Pull Request / build-and-deploy-ci (pull_request) Has been skipped
Pull Request / analyze-bump-type (pull_request) Has been skipped
The build-and-deploy-ci job failed because /apps/euchre_camp_ci/docker-compose.yml
was not accessible inside the job container. Add volume mounts for /apps and the
docker socket so the job can read the CI compose file and run docker compose commands.

Requires runner config.yaml with valid_volumes for /apps and /var/run/docker.sock.
2026-05-16 19:57:48 -07:00
david 861e14503b feat: SDLC database separation for CI/testing (#35)
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 20s
## Summary
- Fix `isProductionDatabase()` to allow CI database (`euchre_camp_ci`)
- Add database schema reset before CI test runs
- Create `global.teardown.ts` for cleanup (CI: full reset, dev/prod: selective cleanup)
- Add `acceptance-tests` job to PR workflow with CI database
- Create `sync-prod-to-dev.js` script for one-way prod→dev sync
- Add `just` recipes: `sync-dev`, `test-prod`, `reset-ci-db`
- Store credentials in `.credentials` (gitignored) with unique CI user

## Testing
Verified against CI database:
- Schema reset works
- Migrations apply correctly
- Test users created successfully
- 219 tests pass (slow but working)

## Next Steps
- Set `CI_DATABASE_URL` as Gitea repository variable

Reviewed-on: #35
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
2026-05-11 06:40:05 +00:00
6 changed files with 41 additions and 43 deletions
+1 -3
View File
@@ -52,11 +52,9 @@ jobs:
- name: Build and push CI base image - name: Build and push CI base image
run: | run: |
WORKSPACE_DIR="$GITHUB_WORKSPACE"
# Build with multiple tags # Build with multiple tags
docker build \ docker build \
--context "$WORKSPACE_DIR" \ --file Dockerfile.ci-base \
--file "$WORKSPACE_DIR/Dockerfile.ci-base" \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:latest \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:latest \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:playwright-${{ steps.meta.outputs.playwright_version }} \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:playwright-${{ steps.meta.outputs.playwright_version }} \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:${{ github.sha }} \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/ci-base:${{ github.sha }} \
+4 -4
View File
@@ -37,6 +37,9 @@ jobs:
container: container:
image: docker.notsosm.art/euchre-camp/ci-base:latest image: docker.notsosm.art/euchre-camp/ci-base:latest
options: --user root options: --user root
volumes:
- /apps:/apps
- /var/run/docker.sock:/var/run/docker.sock
steps: steps:
- name: Checkout code - name: Checkout code
@@ -58,15 +61,12 @@ jobs:
- name: Build Docker image for PR - name: Build Docker image for PR
run: | run: |
WORKSPACE_DIR="$GITHUB_WORKSPACE"
IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}" IMAGE_TAG="pr-${{ steps.info.outputs.pr_number }}-${{ steps.info.outputs.short_sha }}"
docker build \ docker build \
--context "$WORKSPACE_DIR" \
--file "$WORKSPACE_DIR/Dockerfile" \
--target runner \ --target runner \
--build-arg GIT_COMMIT=$GITHUB_SHA \ --build-arg GIT_COMMIT=$GITHUB_SHA \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \ -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \
"$WORKSPACE_DIR" .
- name: Update CI site compose and restart - name: Update CI site compose and restart
run: | run: |
+2 -8
View File
@@ -110,14 +110,11 @@ jobs:
- name: Build test-capable image - name: Build test-capable image
if: steps.commit.outputs.committed == 'true' if: steps.commit.outputs.committed == 'true'
run: | run: |
WORKSPACE_DIR="$GITHUB_WORKSPACE"
docker build \ docker build \
--context "$WORKSPACE_DIR" \
--file "$WORKSPACE_DIR/Dockerfile" \
--target test-runner \ --target test-runner \
--build-arg GIT_COMMIT=${{ github.sha }} \ --build-arg GIT_COMMIT=${{ github.sha }} \
-t ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \ -t ${{ env.IMAGE_NAME }}-test:${{ steps.version.outputs.new_version }} \
"$WORKSPACE_DIR" .
- name: Run tests inside test-capable container - name: Run tests inside test-capable container
if: steps.commit.outputs.committed == 'true' if: steps.commit.outputs.committed == 'true'
@@ -130,15 +127,12 @@ jobs:
- name: Build production image - name: Build production image
if: steps.commit.outputs.committed == 'true' if: steps.commit.outputs.committed == 'true'
run: | run: |
WORKSPACE_DIR="$GITHUB_WORKSPACE"
docker build \ docker build \
--context "$WORKSPACE_DIR" \
--file "$WORKSPACE_DIR/Dockerfile" \
--target runner \ --target runner \
--build-arg GIT_COMMIT=${{ github.sha }} \ --build-arg GIT_COMMIT=${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }} \ -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.new_version }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
"$WORKSPACE_DIR" .
- name: Push Docker images - name: Push Docker images
if: steps.commit.outputs.committed == 'true' if: steps.commit.outputs.committed == 'true'
+1 -3
View File
@@ -47,9 +47,7 @@ describe('getSession', () => {
}) })
it('returns null when an error occurs', async () => { it('returns null when an error occurs', async () => {
mockFetch.mockImplementation(async () => { mockFetch.mockRejectedValue(new Error('Network error'))
throw new Error('Network error')
})
const result = await getSession() const result = await getSession()
+8 -8
View File
@@ -1,6 +1,6 @@
/** /**
* Unit Tests: Permissions * Unit Tests: Permissions
* *
* Tests the permission system for tournament management * Tests the permission system for tournament management
*/ */
@@ -57,7 +57,7 @@ describe('Permissions', () => {
user: { id: '1', email: 'test@example.com' }, user: { id: '1', email: 'test@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('1', 'test@example.com', 'club_admin') createMockUser('1', 'test@example.com', 'club_admin')
); );
@@ -70,7 +70,7 @@ describe('Permissions', () => {
user: { id: '1', email: 'test@example.com' }, user: { id: '1', email: 'test@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('1', 'test@example.com', 'player') createMockUser('1', 'test@example.com', 'player')
); );
@@ -93,7 +93,7 @@ describe('Permissions', () => {
user: { id: 'admin-1', email: 'admin@example.com' }, user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('admin-1', 'admin@example.com', 'club_admin') createMockUser('admin-1', 'admin@example.com', 'club_admin')
); );
@@ -106,7 +106,7 @@ describe('Permissions', () => {
user: { id: 'player-1', email: 'player@example.com' }, user: { id: 'player-1', email: 'player@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('player-1', 'player@example.com', 'player') createMockUser('player-1', 'player@example.com', 'player')
); );
@@ -122,7 +122,7 @@ describe('Permissions', () => {
user: { id: 'admin-1', email: 'admin@example.com' }, user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('admin-1', 'admin@example.com', 'tournament_admin') createMockUser('admin-1', 'admin@example.com', 'tournament_admin')
); );
@@ -135,7 +135,7 @@ describe('Permissions', () => {
user: { id: 'admin-1', email: 'admin@example.com' }, user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('admin-1', 'admin@example.com', 'club_admin') createMockUser('admin-1', 'admin@example.com', 'club_admin')
); );
@@ -148,7 +148,7 @@ describe('Permissions', () => {
user: { id: 'player-1', email: 'player@example.com' }, user: { id: 'player-1', email: 'player@example.com' },
session: { token: 'test', expiresAt: new Date() } session: { token: 'test', expiresAt: new Date() }
})); }));
userFindUniqueMock.mockImplementation(async () => userFindUniqueMock.mockImplementation(async () =>
createMockUser('player-1', 'player@example.com', 'player') createMockUser('player-1', 'player@example.com', 'player')
); );
+25 -17
View File
@@ -3,17 +3,37 @@
* Tests the allowTies field is properly saved when updating tournaments * Tests the allowTies field is properly saved when updating tournaments
*/ */
import { describe, it, expect, mock, beforeEach, afterAll } from 'bun:test'; import { describe, it, expect, mock, beforeEach,} from 'bun:test';
// Create mock functions at module level // Create mock functions at module level
const eventFindUniqueMock = mock(async () => ({})); const eventFindUniqueMock = mock(async () => ({}));
const eventUpdateMock = mock(async () => ({})); const eventUpdateMock = mock(async () => ({}));
const canManageTournamentMock = mock(async () => ({ allowed: true })); const userFindUniqueMock = mock(async () => ({
const canDeleteTournamentMock = mock(async () => ({ allowed: true })); id: 'admin-1',
email: 'admin@example.com',
role: 'club_admin',
emailVerified: false,
name: null,
image: null,
playerId: null,
createdAt: new Date(),
updatedAt: new Date(),
}));
// Mock prisma first // Mock auth-simple to return a valid session
mock.module('@/lib/auth-simple', () => ({
getSession: mock(async () => ({
user: { id: 'admin-1', email: 'admin@example.com' },
session: { token: 'test', expiresAt: new Date() }
})),
}));
// Mock prisma with user and event
mock.module('@/lib/prisma', () => ({ mock.module('@/lib/prisma', () => ({
prisma: { prisma: {
user: {
findUnique: userFindUniqueMock,
},
event: { event: {
findUnique: eventFindUniqueMock, findUnique: eventFindUniqueMock,
update: eventUpdateMock, update: eventUpdateMock,
@@ -21,17 +41,6 @@ mock.module('@/lib/prisma', () => ({
}, },
})); }));
// Mock the permissions module
mock.module('@/lib/permissions', () => ({
canManageTournament: canManageTournamentMock,
canDeleteTournament: canDeleteTournamentMock,
}));
// Cleanup after all tests in this file
afterAll(() => {
mock.restore('module');
});
// Import the route handler after mocking // Import the route handler after mocking
import { PUT } from '@/app/api/tournaments/[id]/route'; import { PUT } from '@/app/api/tournaments/[id]/route';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
@@ -41,8 +50,7 @@ describe('Tournament Update API', () => {
// Clear all mock history before each test // Clear all mock history before each test
eventFindUniqueMock.mockClear(); eventFindUniqueMock.mockClear();
eventUpdateMock.mockClear(); eventUpdateMock.mockClear();
canManageTournamentMock.mockClear(); userFindUniqueMock.mockClear();
canDeleteTournamentMock.mockClear();
}); });
it('should update allowTies field when provided', async () => { it('should update allowTies field when provided', async () => {