Flutter 앱 골격 · 5탭 네비게이션 · 로그인 화면 · Firestore 보안 규칙·인덱스
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
collection,
|
||||
getDocs,
|
||||
orderBy,
|
||||
query,
|
||||
where,
|
||||
} from 'firebase/firestore';
|
||||
import {
|
||||
createTestEnvironment,
|
||||
seedDocuments,
|
||||
} from '../support/environment.mjs';
|
||||
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(new URL('../../firestore.indexes.json', import.meta.url), 'utf8'),
|
||||
);
|
||||
const deliberatelyMissingFields = [
|
||||
['fandomId', 'ASCENDING'],
|
||||
['visibility', 'ASCENDING'],
|
||||
['createdAt', 'DESCENDING'],
|
||||
['likeCount', 'DESCENDING'],
|
||||
];
|
||||
const hasDeliberatelyMissingIndex = manifest.indexes.some((index) => {
|
||||
if (index.collectionGroup !== 'posts' || index.fields.length !== deliberatelyMissingFields.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return deliberatelyMissingFields.every(
|
||||
([fieldPath, order], position) =>
|
||||
index.fields[position].fieldPath === fieldPath && index.fields[position].order === order,
|
||||
);
|
||||
});
|
||||
|
||||
if (hasDeliberatelyMissingIndex) {
|
||||
throw new Error('Probe precondition failed: deliberately missing composite index exists');
|
||||
}
|
||||
|
||||
console.log('INDEX MANIFEST CONTROL PASS: required 4-field posts composite index -> absent');
|
||||
|
||||
const testEnvironment = await createTestEnvironment();
|
||||
|
||||
try {
|
||||
await testEnvironment.clearFirestore();
|
||||
await seedDocuments(testEnvironment, {
|
||||
'posts/post-1': {
|
||||
fandomId: 'fandom-a',
|
||||
visibility: 'visible',
|
||||
createdAt: '2026-08-08T00:00:00Z',
|
||||
likeCount: 1,
|
||||
},
|
||||
'posts/post-2': {
|
||||
fandomId: 'fandom-a',
|
||||
visibility: 'visible',
|
||||
createdAt: '2026-08-08T01:00:00Z',
|
||||
likeCount: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const firestore = testEnvironment.authenticatedContext('alice').firestore();
|
||||
const missingIndexQuery = query(
|
||||
collection(firestore, 'posts'),
|
||||
where('fandomId', '==', 'fandom-a'),
|
||||
where('visibility', '==', 'visible'),
|
||||
orderBy('createdAt', 'desc'),
|
||||
orderBy('likeCount', 'desc'),
|
||||
);
|
||||
const result = await getDocs(missingIndexQuery);
|
||||
|
||||
console.log(`EMULATOR MISSING-INDEX QUERY RESULT: allowed (${result.size} documents)`);
|
||||
console.log('INDEX VALIDATION VERDICT: unsupported — emulator does not enforce composite indexes');
|
||||
} finally {
|
||||
await testEnvironment.cleanup();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { doc, getDoc, setDoc } from 'firebase/firestore';
|
||||
import { assertSucceeds } from '@firebase/rules-unit-testing';
|
||||
import {
|
||||
createTestEnvironment,
|
||||
seedDocuments,
|
||||
userDocument,
|
||||
} from '../support/environment.mjs';
|
||||
|
||||
const testEnvironment = await createTestEnvironment();
|
||||
|
||||
try {
|
||||
await testEnvironment.clearFirestore();
|
||||
|
||||
const uid = 'negative-control-user';
|
||||
const userDb = testEnvironment.authenticatedContext(uid).firestore();
|
||||
let denial;
|
||||
|
||||
try {
|
||||
await setDoc(doc(userDb, `users/${uid}/wallet/main`), { balance: 999_999 });
|
||||
} catch (error) {
|
||||
denial = error;
|
||||
}
|
||||
|
||||
if (denial?.code !== 'permission-denied') {
|
||||
throw new Error(
|
||||
`Negative control failed: expected permission-denied, received ${denial?.code ?? 'success'}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('NEGATIVE CONTROL PASS: authenticated wallet write -> permission-denied');
|
||||
|
||||
await seedDocuments(testEnvironment, {
|
||||
[`users/${uid}`]: userDocument(uid),
|
||||
[`users/${uid}/wallet/main`]: { balance: 100 },
|
||||
});
|
||||
await assertSucceeds(getDoc(doc(userDb, `users/${uid}/wallet/main`)));
|
||||
console.log('REACHABILITY CONTROL PASS: owner wallet read -> allowed');
|
||||
} finally {
|
||||
await testEnvironment.cleanup();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user