-
Notifications
You must be signed in to change notification settings - Fork 0
[codex] phase5 pre-b schema and rules #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ad8cc8f
e761434
a26b80c
c153cad
074fe73
0395002
aea62f5
c1b1b60
b2151db
1a7f115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing' | ||
| import { collection, doc, getDocs, setDoc, addDoc } from 'firebase/firestore' | ||
| import { collection, doc, getDoc, getDocs, setDoc, addDoc } from 'firebase/firestore' | ||
| import { afterAll, beforeAll, describe, it } from 'vitest' | ||
| import { authed, createTestEnv } from '../helpers/rules-harness.js' | ||
| import { seedActiveAccount, staffClaims, ts } from '../helpers/seed-factories.js' | ||
|
|
@@ -13,6 +13,11 @@ beforeAll(async () => { | |
| role: 'municipal_admin', | ||
| municipalityId: 'daet', | ||
| }) | ||
| await seedActiveAccount(env, { | ||
| uid: 'other-admin', | ||
| role: 'municipal_admin', | ||
| municipalityId: 'mercedes', | ||
| }) | ||
| }) | ||
|
|
||
| afterAll(async () => { | ||
|
|
@@ -134,20 +139,146 @@ describe('coordination collections rules', () => { | |
| }) | ||
|
|
||
| it('muni admin can read own municipality requests', async () => { | ||
| const unauthed = env.unauthenticatedContext().firestore() | ||
| await setDoc(doc(unauthed, 'agency_assistance_requests/req-1'), { | ||
| requestedByMunicipality: 'daet', | ||
| targetAgencyId: 'bfp-daet', | ||
| dispatchId: 'd-1', | ||
| requestType: 'BFP', | ||
| requestedAt: ts, | ||
| await env.withSecurityRulesDisabled(async (ctx) => { | ||
| await setDoc(doc(ctx.firestore(), 'agency_assistance_requests', 'req-1'), { | ||
| requestedByMunicipality: 'daet', | ||
| targetAgencyId: 'bfp-daet', | ||
| dispatchId: 'd-1', | ||
| requestType: 'BFP', | ||
| requestedAt: ts, | ||
| }) | ||
| }) | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'agency_assistance_requests', 'req-1'))) | ||
| }) | ||
| }) | ||
|
|
||
| describe('command_channel_threads/messages participant lookup', () => { | ||
| beforeAll(async () => { | ||
| await env.withSecurityRulesDisabled(async (ctx) => { | ||
| await setDoc(doc(ctx.firestore(), 'command_channel_threads', 'thread-1'), { | ||
| threadId: 'thread-1', | ||
| reportId: 'report-1', | ||
| threadType: 'agency_assistance', | ||
| subject: 'Need help', | ||
| participantUids: { 'daet-admin': true }, | ||
| createdBy: 'daet-admin', | ||
| createdAt: ts, | ||
| updatedAt: ts, | ||
| schemaVersion: 1, | ||
| }) | ||
|
|
||
| await setDoc(doc(ctx.firestore(), 'command_channel_messages', 'msg-1'), { | ||
| threadId: 'thread-1', | ||
| authorUid: 'daet-admin', | ||
| authorRole: 'municipal_admin', | ||
| body: 'hello', | ||
| createdAt: ts, | ||
| schemaVersion: 1, | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| it('allows participant to read thread', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDocs(collection(db, 'agency_assistance_requests'))) | ||
| await assertSucceeds(getDoc(doc(db, 'command_channel_threads', 'thread-1'))) | ||
| }) | ||
|
|
||
| it('denies non-participant from reading thread', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'other-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'mercedes' }), | ||
| ) | ||
| await assertFails(getDoc(doc(db, 'command_channel_threads', 'thread-1'))) | ||
| }) | ||
|
|
||
| it('allows participant to read message through parent thread lookup', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'command_channel_messages', 'msg-1'))) | ||
| }) | ||
|
|
||
| it('denies non-participant from reading message', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'other-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'mercedes' }), | ||
| ) | ||
| await assertFails(getDoc(doc(db, 'command_channel_messages', 'msg-1'))) | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('command_channel_threads/messages — participant map key lookup', () => { | ||
| beforeAll(async () => { | ||
| await env.withSecurityRulesDisabled(async (ctx) => { | ||
| await setDoc(doc(ctx.firestore(), 'command_channel_threads', 'thread-1'), { | ||
| threadId: 'thread-1', | ||
| reportId: 'report-1', | ||
| threadType: 'agency_assistance', | ||
| subject: 'Need help', | ||
| participantUids: { 'daet-admin': true }, | ||
| createdBy: 'daet-admin', | ||
| createdAt: ts, | ||
| updatedAt: ts, | ||
| schemaVersion: 1, | ||
| }) | ||
|
|
||
| await setDoc(doc(ctx.firestore(), 'command_channel_messages', 'msg-1'), { | ||
| threadId: 'thread-1', | ||
| message: 'hello', | ||
| sentBy: 'daet-admin', | ||
| sentAt: ts, | ||
| schemaVersion: 1, | ||
| }) | ||
|
Comment on lines
+292
to
+298
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Message document fields inconsistent with schema. The While Firestore rules don't enforce schema, using inconsistent test data may:
💡 Suggested alignment with schema await setDoc(doc(ctx.firestore(), 'command_channel_messages', 'msg-2'), {
threadId: 'thread-2',
- message: 'hello',
- sentBy: 'daet-admin',
- sentAt: ts,
+ authorUid: 'daet-admin',
+ authorRole: 'municipal_admin',
+ body: 'hello',
+ createdAt: ts,
schemaVersion: 1,
})🤖 Prompt for AI Agents |
||
| }) | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| it('allows participant to read thread', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'command_channel_threads', 'thread-1'))) | ||
| }) | ||
|
|
||
| it('denies non-participant from reading thread', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'other-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'mercedes' }), | ||
| ) | ||
| await assertFails(getDoc(doc(db, 'command_channel_threads', 'thread-1'))) | ||
| }) | ||
|
|
||
| it('allows participant to read a message when parent thread participantUids contains uid', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'command_channel_messages', 'msg-1'))) | ||
| }) | ||
|
|
||
| it('denies non-participant from reading a message', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'other-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'mercedes' }), | ||
| ) | ||
| await assertFails(getDoc(doc(db, 'command_channel_messages', 'msg-1'))) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing' | ||
| import { doc, getDoc, setDoc } from 'firebase/firestore' | ||
| import { afterAll, beforeAll, describe, it } from 'vitest' | ||
| import { authed, createTestEnv, unauthed } from '../helpers/rules-harness.js' | ||
| import { seedActiveAccount, staffClaims, ts } from '../helpers/seed-factories.js' | ||
|
|
||
| let env: Awaited<ReturnType<typeof createTestEnv>> | ||
|
|
||
| const sessionData = { | ||
| uid: 'daet-admin', | ||
| municipalityId: 'daet', | ||
| enteredAt: ts, | ||
| expiresAt: ts + 43200000, | ||
| isActive: true, | ||
| schemaVersion: 1, | ||
| } | ||
|
|
||
| beforeAll(async () => { | ||
| env = await createTestEnv('field-mode-sessions-rules-test') | ||
| await seedActiveAccount(env, { | ||
| uid: 'daet-admin', | ||
| role: 'municipal_admin', | ||
| municipalityId: 'daet', | ||
| }) | ||
| await seedActiveAccount(env, { | ||
| uid: 'other-admin', | ||
| role: 'municipal_admin', | ||
| municipalityId: 'mercedes', | ||
| }) | ||
| await seedActiveAccount(env, { uid: 'superadmin', role: 'provincial_superadmin' }) | ||
|
|
||
| await env.withSecurityRulesDisabled(async (ctx) => { | ||
| await setDoc(doc(ctx.firestore(), 'field_mode_sessions', 'daet-admin'), sessionData) | ||
| }) | ||
| }) | ||
|
|
||
| afterAll(async () => { | ||
| await env.cleanup() | ||
| }) | ||
|
|
||
| describe('field_mode_sessions rules', () => { | ||
| it('allows owner to read their own session', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'field_mode_sessions', 'daet-admin'))) | ||
| }) | ||
|
|
||
| it('allows owner to write their own session', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(setDoc(doc(db, 'field_mode_sessions', 'daet-admin'), sessionData)) | ||
| }) | ||
|
|
||
| it('denies other user reading another user session', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'other-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'mercedes' }), | ||
| ) | ||
| await assertFails(getDoc(doc(db, 'field_mode_sessions', 'daet-admin'))) | ||
| }) | ||
|
|
||
| it('denies unauthenticated reads', async () => { | ||
| const db = unauthed(env) | ||
| await assertFails(getDoc(doc(db, 'field_mode_sessions', 'daet-admin'))) | ||
| }) | ||
|
|
||
| it('denies superadmin writes to field_mode_sessions', async () => { | ||
| const db = authed(env, 'superadmin', staffClaims({ role: 'provincial_superadmin' })) | ||
| await assertFails(setDoc(doc(db, 'field_mode_sessions', 'daet-admin'), sessionData)) | ||
| }) | ||
|
|
||
| it('allows superadmin reads', async () => { | ||
| const db = authed(env, 'superadmin', staffClaims({ role: 'provincial_superadmin' })) | ||
| await assertSucceeds(getDoc(doc(db, 'field_mode_sessions', 'daet-admin'))) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing' | ||
| import { addDoc, collection, doc, getDoc, setDoc } from 'firebase/firestore' | ||
| import { afterAll, beforeAll, describe, it } from 'vitest' | ||
| import { authed, createTestEnv } from '../helpers/rules-harness.js' | ||
| import { seedActiveAccount, seedReport, staffClaims, ts } from '../helpers/seed-factories.js' | ||
|
|
||
| let env: Awaited<ReturnType<typeof createTestEnv>> | ||
|
|
||
| beforeAll(async () => { | ||
| env = await createTestEnv('report-messages-rules-test') | ||
| await seedActiveAccount(env, { | ||
| uid: 'daet-admin', | ||
| role: 'municipal_admin', | ||
| municipalityId: 'daet', | ||
| }) | ||
| await seedActiveAccount(env, { | ||
| uid: 'bfp-admin', | ||
| role: 'agency_admin', | ||
| municipalityId: 'daet', | ||
| agencyId: 'bfp-daet', | ||
| }) | ||
| await seedReport(env, 'report-1', { | ||
| municipalityId: 'daet', | ||
| opsOverrides: { municipalityId: 'daet', agencyIds: ['bfp-daet'] }, | ||
| }) | ||
| await env.withSecurityRulesDisabled(async (ctx) => { | ||
| await setDoc(doc(ctx.firestore(), 'reports', 'report-1', 'messages', 'msg-1'), { | ||
| authorUid: 'daet-admin', | ||
| body: 'Seed message', | ||
| createdAt: ts, | ||
| schemaVersion: 1, | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| afterAll(async () => { | ||
| await env.cleanup() | ||
| }) | ||
|
|
||
| describe('reports/messages rules', () => { | ||
| it('allows muni admin to read a message', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'reports', 'report-1', 'messages', 'msg-1'))) | ||
| }) | ||
|
|
||
| it('allows agency admin to read a message when report_ops agencyIds includes their agency', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'bfp-admin', | ||
| staffClaims({ role: 'agency_admin', municipalityId: 'daet', agencyId: 'bfp-daet' }), | ||
| ) | ||
| await assertSucceeds(getDoc(doc(db, 'reports', 'report-1', 'messages', 'msg-1'))) | ||
| }) | ||
|
Comment on lines
+50
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding agency admin read denial test. The test verifies an agency admin with matching 📝 Suggested test additionit('denies agency admin read when their agency is not in report_ops.agencyIds', async () => {
await seedActiveAccount(env, {
uid: 'pnp-admin',
role: 'agency_admin',
municipalityId: 'daet',
agencyId: 'pnp-daet', // Different agency
})
const db = authed(
env,
'pnp-admin',
staffClaims({ role: 'agency_admin', municipalityId: 'daet', agencyId: 'pnp-daet' }),
)
// report-1 only has bfp-daet in agencyIds
await assertFails(getDoc(doc(db, 'reports', 'report-1', 'messages', 'msg-1')))
})🤖 Prompt for AI Agents🧹 Nitpick | 🔵 Trivial Missing agency admin write test. The test verifies agency admin can read messages but doesn't test that agency admin can create messages when their agency is in 📝 Suggested test additionit('allows agency admin to write a message when report_ops agencyIds includes their agency', async () => {
const db = authed(
env,
'bfp-admin',
staffClaims({ role: 'agency_admin', municipalityId: 'daet', agencyId: 'bfp-daet' }),
)
await assertSucceeds(
addDoc(collection(db, 'reports', 'report-1', 'messages'), {
authorUid: 'bfp-admin',
body: 'Agency response.',
createdAt: ts,
schemaVersion: 1,
}),
)
})🤖 Prompt for AI Agents |
||
|
|
||
| it('allows muni admin to write a message', async () => { | ||
| const db = authed( | ||
| env, | ||
| 'daet-admin', | ||
| staffClaims({ role: 'municipal_admin', municipalityId: 'daet' }), | ||
| ) | ||
| await assertSucceeds( | ||
| addDoc(collection(db, 'reports', 'report-1', 'messages'), { | ||
| authorUid: 'daet-admin', | ||
| body: 'En route.', | ||
| createdAt: ts, | ||
| schemaVersion: 1, | ||
| }), | ||
| ) | ||
| }) | ||
|
|
||
| it('denies citizen writes to messages', async () => { | ||
| const db = authed(env, 'citizen-1', staffClaims({ role: 'citizen' })) | ||
| await assertFails( | ||
| addDoc(collection(db, 'reports', 'report-1', 'messages'), { | ||
| authorUid: 'citizen-1', | ||
| body: 'hi', | ||
| createdAt: ts, | ||
| schemaVersion: 1, | ||
| }), | ||
| ) | ||
| }) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding unauthenticated read denial tests.
The participant lookup tests cover authorized/unauthorized participants but don't explicitly test unauthenticated access denial. While
isActivePrivileged()implicitly requires authentication, explicit tests would strengthen coverage.📝 Suggested test addition
Note: Requires importing
unauthedfrom../helpers/rules-harness.js.🤖 Prompt for AI Agents