fix(rd21): retain draft revisions and qualify conflict recovery
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Read only the explicitly named synthetic owner's committed SQLite rows."""
|
||||
import json
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
CHILDREN = ("CareerExperiences", "CareerEducations", "CareerSkills", "CareerProjects",
|
||||
"CareerCertifications", "CareerLanguages")
|
||||
|
||||
|
||||
def connect_readonly(filename):
|
||||
path = Path(filename).resolve(strict=True)
|
||||
connection = sqlite3.connect(path.as_uri() + "?mode=ro", uri=True)
|
||||
connection.row_factory = sqlite3.Row
|
||||
return connection
|
||||
|
||||
|
||||
def snapshot(connection, owner, job_id=None):
|
||||
def rows(sql, parameters):
|
||||
return [dict(row) for row in connection.execute(sql, parameters)]
|
||||
|
||||
identities = rows('SELECT "Id", "ProfileCvText", "ProfileCvStructureJson", '
|
||||
'"CurrentCvProfileVersion" FROM "AspNetUsers" WHERE "Id" = ?', (owner,))
|
||||
if len(identities) != 1:
|
||||
raise ValueError("Expected exactly one explicitly owned synthetic Identity row")
|
||||
careers = rows('SELECT * FROM "CareerProfiles" WHERE "OwnerUserId" = ?', (owner,))
|
||||
if len(careers) > 1:
|
||||
raise ValueError("More than one canonical Career Profile for owner")
|
||||
result = {
|
||||
"identity": identities[0],
|
||||
"career": careers[0] if careers else None,
|
||||
"careerHistory": rows('SELECT * FROM "CareerProfileVersions" WHERE "OwnerUserId" = ? ORDER BY "Version", "Id"', (owner,)),
|
||||
"children": {table: rows(f'SELECT * FROM "{table}" WHERE "OwnerUserId" = ? ORDER BY "SortOrder", "Id"', (owner,)) for table in CHILDREN},
|
||||
"job": None, "opportunity": None, "events": [],
|
||||
}
|
||||
if job_id is not None:
|
||||
jobs = rows('SELECT * FROM "JobApplications" WHERE "OwnerUserId" = ? AND "Id" = ?', (owner, job_id))
|
||||
if len(jobs) != 1:
|
||||
raise ValueError("Expected exactly one explicitly owned synthetic job")
|
||||
result["job"] = jobs[0]
|
||||
opportunities = rows('SELECT * FROM "Jobs" WHERE "OwnerUserId" = ? AND "Id" = ?', (owner, jobs[0]["JobId"]))
|
||||
if len(opportunities) != 1:
|
||||
raise ValueError("Expected the job's explicitly owned opportunity")
|
||||
result["opportunity"] = opportunities[0]
|
||||
result["events"] = rows('SELECT e.* FROM "JobEvents" e JOIN "JobApplications" j ON e."JobApplicationId" = j."Id" '
|
||||
'WHERE j."OwnerUserId" = ? AND j."Id" = ? ORDER BY e."Id"', (owner, job_id))
|
||||
return result
|
||||
|
||||
|
||||
def self_test():
|
||||
with tempfile.TemporaryDirectory(prefix="rd21-readonly-proof-") as directory:
|
||||
filename = Path(directory) / "jobtracker.db"
|
||||
with closing(sqlite3.connect(filename)) as writer, writer:
|
||||
writer.executescript('''
|
||||
CREATE TABLE AspNetUsers (Id TEXT, ProfileCvText TEXT, ProfileCvStructureJson TEXT, CurrentCvProfileVersion INTEGER);
|
||||
INSERT INTO AspNetUsers VALUES ('owner', 'raw', '{}', 1), ('other', 'private', '{}', 1);
|
||||
CREATE TABLE CareerProfiles (Id INTEGER, OwnerUserId TEXT, Version INTEGER, ProfileJson TEXT);
|
||||
INSERT INTO CareerProfiles VALUES (1, 'owner', 1, '{}'), (2, 'other', 1, '{}');
|
||||
CREATE TABLE CareerProfileVersions (Id INTEGER, OwnerUserId TEXT, Version INTEGER);
|
||||
INSERT INTO CareerProfileVersions VALUES (1, 'owner', 1), (2, 'other', 1);
|
||||
CREATE TABLE JobApplications (Id INTEGER, OwnerUserId TEXT, JobId INTEGER, Version INTEGER);
|
||||
INSERT INTO JobApplications VALUES (1, 'owner', 10, 2), (2, 'other', 20, 3);
|
||||
CREATE TABLE Jobs (Id INTEGER, OwnerUserId TEXT);
|
||||
INSERT INTO Jobs VALUES (10, 'owner'), (20, 'other');
|
||||
CREATE TABLE JobEvents (Id INTEGER, JobApplicationId INTEGER);
|
||||
INSERT INTO JobEvents VALUES (1, 1), (2, 2);
|
||||
''')
|
||||
for table in CHILDREN:
|
||||
writer.execute(f'CREATE TABLE "{table}" (Id INTEGER, OwnerUserId TEXT, SortOrder INTEGER)')
|
||||
writer.executemany(f'INSERT INTO "{table}" VALUES (?, ?, ?)', [(1, "owner", 0), (2, "other", 0)])
|
||||
with closing(connect_readonly(filename)) as reader:
|
||||
before = snapshot(reader, "owner", 1)
|
||||
assert before["career"]["Version"] == 1
|
||||
assert before["job"]["Version"] == 2
|
||||
assert before["opportunity"]["Id"] == 10
|
||||
assert before["events"] == [{"Id": 1, "JobApplicationId": 1}]
|
||||
assert all(len(rows) == 1 for rows in before["children"].values())
|
||||
assert "private" not in json.dumps(before)
|
||||
try:
|
||||
reader.execute('UPDATE CareerProfiles SET Version = 99')
|
||||
except sqlite3.OperationalError as error:
|
||||
assert "readonly" in str(error).lower()
|
||||
else:
|
||||
raise AssertionError("Read-only URI allowed mutation")
|
||||
assert snapshot(reader, "owner", 1) == before
|
||||
try:
|
||||
snapshot(reader, "owner", 2)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Cross-owner job was returned")
|
||||
print("readonly-db-proof self-test passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1:] == ["--self-test"]:
|
||||
self_test()
|
||||
else:
|
||||
if len(sys.argv) not in (3, 4):
|
||||
raise SystemExit("Usage: readonly-db-proof.py DATABASE OWNER_ID [JOB_ID] | --self-test")
|
||||
with closing(connect_readonly(sys.argv[1])) as connection:
|
||||
connection.execute("BEGIN")
|
||||
print(json.dumps(snapshot(connection, sys.argv[2], int(sys.argv[3]) if len(sys.argv) == 4 else None)))
|
||||
Reference in New Issue
Block a user