feat(mobile): identité user-first — login/password, SecureStore, scoping par compte
- api: DeviceRegistration device-only, login/changePassword/resolveUser, interception 401 (purge) exonérée sur le login ; type User aligné sur le fil (is_admin) - SecureStore (expo-secure-store v57) : token + profil, jamais en SQLite ; miroir active_user_id - AuthContext : statut loading/signedOut/signedIn, bootstrap register→restore, signIn/signOut, garde-fou switch de compte - app/login.tsx + gate de routes dans _layout (Redirect signedOut) ; i18n fr/en - DB v5 : user_id sur pending_operations + resource_permissions (UNIQUE par user), repos scopés (user_id IS ? OR IS NULL) - syncOutbox : delta permissions par compte (Map), outbox poussée du compte actif uniquement - tests: register/login/401/resolve/changePassword, scoping outbox+permissions, migrations v5 ; e2e live revert register→login admin
This commit is contained in:
@@ -80,7 +80,7 @@ async function seedLegacyTree(h: Harness): Promise<void> {
|
||||
VALUES ('${SONG_URI}', 'song.mp3', '${MUSIC_URI}', 'mp3', 30, 'audio/mpeg', 0, 3000, 'cloud', 3000);`);
|
||||
}
|
||||
|
||||
test('fresh migrate v0 → v4 creates full schema and seeds device id', async () => {
|
||||
test('fresh migrate v0 → v5 creates full schema and seeds device id', async () => {
|
||||
const h = createHarness();
|
||||
await migrateDatabase(h);
|
||||
|
||||
@@ -108,6 +108,11 @@ test('fresh migrate v0 → v4 creates full schema and seeds device id', async ()
|
||||
assert.ok(names.length > 0, `table ${table} must exist`);
|
||||
}
|
||||
|
||||
const permCols = await columnNames(h, 'resource_permissions');
|
||||
assert.ok(permCols.includes('user_id'), 'resource_permissions must have user_id (v5)');
|
||||
const opCols = await columnNames(h, 'pending_operations');
|
||||
assert.ok(opCols.includes('user_id'), 'pending_operations must have user_id (v5)');
|
||||
|
||||
const device = await h.getFirstAsync<{ value: string }>(
|
||||
'SELECT "value" FROM user_preferences WHERE "key" = ?',
|
||||
DEVICE_USER_ID_KEY,
|
||||
@@ -117,6 +122,35 @@ test('fresh migrate v0 → v4 creates full schema and seeds device id', async ()
|
||||
assert.deepEqual(await foreignKeyViolations(h), [], 'no orphaned FKs after fresh migrate');
|
||||
});
|
||||
|
||||
test('v5 : UNIQUE de resource_permissions scopé par (user_id, resource_id, resource_type)', async () => {
|
||||
const h = createHarness();
|
||||
await migrateDatabase(h);
|
||||
|
||||
const row = await h.getFirstAsync<{ sql: string }>(
|
||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND tbl_name = 'resource_permissions'`,
|
||||
);
|
||||
assert.ok(row, 'table sql present');
|
||||
assert.match(row.sql, /UNIQUE\s*\(user_id,\s*resource_id,\s*resource_type\)/);
|
||||
|
||||
// Deux comptes peuvent partager la même ressource sans collision.
|
||||
await h.runAsync(
|
||||
`INSERT INTO resource_permissions (user_id, resource_id, resource_type, effective_access, inherit, owner_id, cached_at, updated_at)
|
||||
VALUES ('u1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'folder', 'owner', 1, NULL, 1, 1)`,
|
||||
);
|
||||
await h.runAsync(
|
||||
`INSERT INTO resource_permissions (user_id, resource_id, resource_type, effective_access, inherit, owner_id, cached_at, updated_at)
|
||||
VALUES ('u2', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'folder', 'viewer', 0, NULL, 1, 1)`,
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
h.runAsync(
|
||||
`INSERT INTO resource_permissions (user_id, resource_id, resource_type, effective_access, inherit, owner_id, cached_at, updated_at)
|
||||
VALUES ('u1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'folder', 'editor', 1, NULL, 1, 1)`,
|
||||
),
|
||||
/UNIQUE constraint failed/,
|
||||
);
|
||||
});
|
||||
|
||||
test('user_version is transactional (rollback restores previous version)', async () => {
|
||||
const h = createHarness();
|
||||
await migrateDatabase(h);
|
||||
|
||||
Reference in New Issue
Block a user