Add immutable social audit records
This commit is contained in:
1 parent
eab282d8b1
commit
a21d35a165
12 files changed
+254
-6
No files matched your search
@@ -0,0 +1,62 @@
|
||||
export const socialAuditRecordsSchema = `
|
||||
create table if not exists social_audit_records (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
social_account_id text references social_accounts(id) on delete set null,
|
||||
source_type text not null,
|
||||
source_id text,
|
||||
event_type text not null,
|
||||
actor_type text not null,
|
||||
actor_id text,
|
||||
policy_decision text not null,
|
||||
evidence_file_id text references file_objects(id) on delete set null,
|
||||
metadata_json text not null,
|
||||
created_at text not null,
|
||||
unique(source_type, source_id, event_type)
|
||||
);
|
||||
|
||||
insert into social_audit_records(
|
||||
id, org_id, social_account_id, source_type, source_id, event_type,
|
||||
actor_type, actor_id, policy_decision, evidence_file_id, metadata_json, created_at
|
||||
)
|
||||
select
|
||||
'saud_' || lower(hex(randomblob(16))),
|
||||
org_id,
|
||||
social_account_id,
|
||||
'browser_action',
|
||||
id,
|
||||
action_type,
|
||||
'system',
|
||||
null,
|
||||
policy_decision,
|
||||
evidence_file_id,
|
||||
json_object('source', 'browser_actions'),
|
||||
created_at
|
||||
from browser_actions
|
||||
where not exists (
|
||||
select 1 from social_audit_records
|
||||
where social_audit_records.source_type = 'browser_action'
|
||||
and social_audit_records.source_id = browser_actions.id
|
||||
and social_audit_records.event_type = browser_actions.action_type
|
||||
);
|
||||
|
||||
create index if not exists idx_social_audit_records_org_created
|
||||
on social_audit_records(org_id, created_at);
|
||||
create index if not exists idx_social_audit_records_account_created
|
||||
on social_audit_records(social_account_id, created_at);
|
||||
|
||||
create trigger if not exists trg_social_audit_records_no_update
|
||||
before update on social_audit_records
|
||||
begin
|
||||
select raise(abort, 'Social audit records are immutable.');
|
||||
end;
|
||||
|
||||
create trigger if not exists trg_social_audit_records_no_delete
|
||||
before delete on social_audit_records
|
||||
begin
|
||||
select raise(abort, 'Social audit records are immutable.');
|
||||
end;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (27, 'social_audit_records', datetime('now'));
|
||||
`;
|
||||
@@ -25,6 +25,7 @@ import { providerRequestRuntimeSchema } from "./migrations/023-provider-request-
|
||||
import { taskRunProviderRequestSchema } from "./migrations/024-task-run-provider-request";
|
||||
import { chatMessageDeliverySchema } from "./migrations/025-chat-message-delivery";
|
||||
import { browserPlatformGovernanceSchema } from "./migrations/026-browser-platform-governance";
|
||||
import { socialAuditRecordsSchema } from "./migrations/027-social-audit-records";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -59,6 +60,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 24, name: "task_run_provider_request", sql: taskRunProviderRequestSchema },
|
||||
{ version: 25, name: "chat_message_delivery", sql: chatMessageDeliverySchema },
|
||||
{ version: 26, name: "browser_platform_governance", sql: browserPlatformGovernanceSchema },
|
||||
{ version: 27, name: "social_audit_records", sql: socialAuditRecordsSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -68,6 +68,7 @@ const LEGACY_TABLES = [
|
||||
"browser_action_policies",
|
||||
"social_approval_routes",
|
||||
"browser_actions",
|
||||
"social_audit_records",
|
||||
"scheduled_posts",
|
||||
"policy_rules",
|
||||
"compliance_exports",
|
||||
@@ -227,6 +228,9 @@ const RELATION_CHECKS: RelationCheck[] = [
|
||||
{ table: "browser_actions", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "browser_actions", column: "social_account_id", referencedTable: "social_accounts", referencedColumn: "id", reason: "dangling_social_account_id" },
|
||||
{ table: "browser_actions", column: "evidence_file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_evidence_file_id" },
|
||||
{ table: "social_audit_records", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "social_audit_records", column: "social_account_id", referencedTable: "social_accounts", referencedColumn: "id", reason: "dangling_social_account_id" },
|
||||
{ table: "social_audit_records", column: "evidence_file_id", referencedTable: "file_objects", referencedColumn: "id", reason: "dangling_evidence_file_id" },
|
||||
{ table: "scheduled_posts", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
|
||||
{ table: "scheduled_posts", column: "social_account_id", referencedTable: "social_accounts", referencedColumn: "id", reason: "dangling_social_account_id" },
|
||||
{ table: "scheduled_posts", column: "approval_id", referencedTable: "approvals", referencedColumn: "id", reason: "dangling_approval_id" },
|
||||
|
||||
@@ -262,7 +262,7 @@ export class SdlcService {
|
||||
{ key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` },
|
||||
{ key: "provider_rate_limit_registry", passed: rateLimitRegistry === 1, value: rateLimitRegistry },
|
||||
{ key: "provider_runtime_contract", passed: runtimeContractReady, value: runtimeContractReady },
|
||||
{ key: "schema_version", passed: schemaVersion >= 26, value: schemaVersion },
|
||||
{ key: "schema_version", passed: schemaVersion >= 27, value: schemaVersion },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ export class SnapshotService {
|
||||
.map((policy) => ({ ...policy, approvalRequired: toBool(policy.approvalRequired) })),
|
||||
socialApprovalRoutes: list(this.db, "social_approval_routes", "created_at asc"),
|
||||
browserActions: list(this.db, "browser_actions", "created_at desc"),
|
||||
socialAuditRecords: list(this.db, "social_audit_records", "created_at desc"),
|
||||
policyRules: list(this.db, "policy_rules", "created_at asc"),
|
||||
migrationReports: list(this.db, "migration_reports", "created_at desc"),
|
||||
rollbackSnapshots,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { createId } from "../../shared/ids";
|
||||
|
||||
type SocialAuditMetadata = Record<string, unknown>;
|
||||
|
||||
export interface RecordSocialAuditInput {
|
||||
orgId: string;
|
||||
socialAccountId: string | null;
|
||||
sourceType: string;
|
||||
sourceId: string | null;
|
||||
eventType: string;
|
||||
actorType: string;
|
||||
actorId: string | null;
|
||||
policyDecision: string;
|
||||
evidenceFileId?: string | null;
|
||||
metadata: SocialAuditMetadata;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export class SocialAuditService {
|
||||
constructor(private readonly db: DatabaseSync) {}
|
||||
|
||||
record(input: RecordSocialAuditInput): string {
|
||||
const id = createId("saud");
|
||||
this.db.prepare(`
|
||||
insert into social_audit_records(
|
||||
id, org_id, social_account_id, source_type, source_id, event_type,
|
||||
actor_type, actor_id, policy_decision, evidence_file_id, metadata_json, created_at
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
input.orgId,
|
||||
input.socialAccountId,
|
||||
requireText(input.sourceType, "Social audit source type is required."),
|
||||
input.sourceId,
|
||||
requireText(input.eventType, "Social audit event type is required."),
|
||||
requireText(input.actorType, "Social audit actor type is required."),
|
||||
input.actorId,
|
||||
requireText(input.policyDecision, "Social audit policy decision is required."),
|
||||
input.evidenceFileId ?? null,
|
||||
JSON.stringify(input.metadata),
|
||||
input.createdAt ?? new Date().toISOString(),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
function requireText(value: string, message: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) throw new Error(message);
|
||||
return trimmed;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { APPROVAL_STATE, RISK_LEVEL, ROUTE_TO_TYPE, SOCIAL_PLATFORM, type RiskLe
|
||||
import { createId } from "../../shared/ids";
|
||||
import { ApprovalService } from "./approval-service";
|
||||
import { AuditService } from "./audit-service";
|
||||
import { SocialAuditService } from "./social-audit-service";
|
||||
import {
|
||||
SOCIAL_BROWSER_ACTION,
|
||||
SocialBrowserPolicyService,
|
||||
@@ -75,17 +76,19 @@ interface GrantScope {
|
||||
export class SocialService {
|
||||
private readonly audit: AuditService;
|
||||
private readonly approvals: ApprovalService;
|
||||
private readonly socialAudit: SocialAuditService;
|
||||
private readonly browserPolicy: SocialBrowserPolicyService;
|
||||
|
||||
constructor(private readonly db: DatabaseSync) {
|
||||
this.audit = new AuditService(db);
|
||||
this.approvals = new ApprovalService(db);
|
||||
this.socialAudit = new SocialAuditService(db);
|
||||
this.browserPolicy = new SocialBrowserPolicyService(db);
|
||||
}
|
||||
|
||||
createScheduledPost(input: CreateScheduledPostInput): string {
|
||||
this.requireAccount(input.orgId, input.socialAccountId);
|
||||
this.browserPolicy.requireActionPolicy(input.orgId, input.socialAccountId, SOCIAL_BROWSER_ACTION.DRAFT);
|
||||
const policy = this.browserPolicy.requireActionPolicy(input.orgId, input.socialAccountId, SOCIAL_BROWSER_ACTION.DRAFT);
|
||||
this.requireGrant(input.orgId, input.socialAccountId, input.createdByEmployeeId, SOCIAL_BROWSER_ACTION.DRAFT);
|
||||
const body = this.requireText(input.bodyMd, "Post body is required.");
|
||||
const scheduledFor = this.requireSchedule(input.scheduledFor);
|
||||
@@ -117,6 +120,18 @@ export class SocialService {
|
||||
action: "drafted",
|
||||
payload: { socialAccountId: input.socialAccountId, scheduledFor, state: "draft" },
|
||||
});
|
||||
this.socialAudit.record({
|
||||
orgId: input.orgId,
|
||||
socialAccountId: input.socialAccountId,
|
||||
sourceType: "scheduled_post",
|
||||
sourceId: id,
|
||||
eventType: "draft_created",
|
||||
actorType: "employee",
|
||||
actorId: input.createdByEmployeeId,
|
||||
policyDecision: "allowed_by_policy",
|
||||
metadata: { scheduledFor, actionType: policy.action_type, riskLevel: policy.risk_level },
|
||||
createdAt: now,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -142,6 +157,18 @@ export class SocialService {
|
||||
action: `state_${input.state}`,
|
||||
payload: { state: input.state, approvalId },
|
||||
});
|
||||
this.socialAudit.record({
|
||||
orgId: input.orgId,
|
||||
socialAccountId: post.social_account_id,
|
||||
sourceType: "scheduled_post",
|
||||
sourceId: input.scheduledPostId,
|
||||
eventType: `state_${input.state}`,
|
||||
actorType: "employee",
|
||||
actorId: input.employeeId,
|
||||
policyDecision: policyDecisionForState(input.state, policy.approval_required === 1),
|
||||
metadata: { state: input.state, approvalId, actionType: policy.action_type, riskLevel: policy.risk_level },
|
||||
createdAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
createAccount(input: CreateSocialAccountInput): string {
|
||||
@@ -198,6 +225,18 @@ export class SocialService {
|
||||
action: "created",
|
||||
payload: { platform: input.platform, handle },
|
||||
});
|
||||
this.socialAudit.record({
|
||||
orgId: input.orgId,
|
||||
socialAccountId: id,
|
||||
sourceType: "social_account",
|
||||
sourceId: id,
|
||||
eventType: "account_created",
|
||||
actorType: "employee",
|
||||
actorId: input.createdByEmployeeId,
|
||||
policyDecision: "allowed_employee_authorization",
|
||||
metadata: { platform: input.platform, handle, authorizedEmployeeId: input.authorizedEmployeeId ?? null },
|
||||
createdAt: now,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -225,10 +264,11 @@ export class SocialService {
|
||||
win.loadURL(PLATFORM_LOGIN_URL[account.platform]);
|
||||
const now = new Date().toISOString();
|
||||
this.browserPolicy.markSessionUsed(container.id, now);
|
||||
const browserActionId = createId("bact");
|
||||
this.db.prepare(`
|
||||
insert into browser_actions(id, org_id, social_account_id, action_type, policy_decision, evidence_file_id, created_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(createId("bact"), account.org_id, account.id, "login_window_opened", "allowed_human_login", null, now);
|
||||
`).run(browserActionId, account.org_id, account.id, "login_window_opened", "allowed_human_login", null, now);
|
||||
this.audit.record({
|
||||
orgId: account.org_id,
|
||||
entityType: "social_account",
|
||||
@@ -238,6 +278,18 @@ export class SocialService {
|
||||
action: "login_window_opened",
|
||||
payload: { platform: account.platform },
|
||||
});
|
||||
this.socialAudit.record({
|
||||
orgId: account.org_id,
|
||||
socialAccountId: account.id,
|
||||
sourceType: "browser_action",
|
||||
sourceId: browserActionId,
|
||||
eventType: "login_window_opened",
|
||||
actorType: "employee",
|
||||
actorId: input.employeeId,
|
||||
policyDecision: "allowed_human_login",
|
||||
metadata: { platform: account.platform, containerId: container.id },
|
||||
createdAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
private attachNavigationPolicy(win: BrowserWindow, allowedHosts: string[]): void {
|
||||
@@ -406,3 +458,9 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
function isRiskLevel(value: unknown): value is RiskLevel {
|
||||
return Object.values(RISK_LEVEL).includes(value as RiskLevel);
|
||||
}
|
||||
|
||||
function policyDecisionForState(state: SetScheduledPostStateInput["state"], approvalRequired: boolean): string {
|
||||
if (state === "scheduled" && approvalRequired) return "approval_requested";
|
||||
if (state === "published" && approvalRequired) return "allowed_after_approval";
|
||||
return "allowed_by_policy";
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type { ProviderRequestRecord } from "./provider-request-domain";
|
||||
import type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain";
|
||||
import type { RollbackSnapshotRecord } from "./rollback-domain";
|
||||
import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain";
|
||||
import type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord } from "./social-domain";
|
||||
import type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord, SocialAuditRecord } from "./social-domain";
|
||||
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain";
|
||||
export type { AuditReplayRunRecord } from "./audit-domain";
|
||||
export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain";
|
||||
@@ -24,7 +24,7 @@ export type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord }
|
||||
export type { RollbackSnapshotRecord } from "./rollback-domain";
|
||||
export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain";
|
||||
export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain";
|
||||
export type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord } from "./social-domain";
|
||||
export type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord, SocialAuditRecord } from "./social-domain";
|
||||
export const TASK_STATUS = { BACKLOG: "backlog", IN_PROGRESS: "in_progress", REVIEW: "review", DONE: "done" } as const;
|
||||
export const APPROVAL_STATE = { PENDING: "pending", APPROVED: "approved", DECLINED: "declined" } as const;
|
||||
export const RISK_LEVEL = { LOW: "low", MEDIUM: "medium", HIGH: "high" } as const;
|
||||
@@ -449,6 +449,7 @@ export interface AppSnapshot {
|
||||
browserActionPolicies: BrowserActionPolicyRecord[];
|
||||
socialApprovalRoutes: SocialApprovalRouteRecord[];
|
||||
browserActions: BrowserActionRecord[];
|
||||
socialAuditRecords: SocialAuditRecord[];
|
||||
policyRules: PolicyRuleRecord[];
|
||||
migrationReports: MigrationReportRecord[];
|
||||
rollbackSnapshots: RollbackSnapshotRecord[];
|
||||
|
||||
@@ -66,3 +66,18 @@ export interface BrowserActionRecord {
|
||||
evidenceFileId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SocialAuditRecord {
|
||||
id: string;
|
||||
orgId: string;
|
||||
socialAccountId: string | null;
|
||||
sourceType: string;
|
||||
sourceId: string | null;
|
||||
eventType: string;
|
||||
actorType: string;
|
||||
actorId: string | null;
|
||||
policyDecision: string;
|
||||
evidenceFileId: string | null;
|
||||
metadataJson: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ describe("migration service", () => {
|
||||
create table browser_action_policies(id text primary key, org_id text, social_account_id text);
|
||||
create table social_approval_routes(id text primary key, org_id text, social_account_id text, route_to_id text);
|
||||
create table browser_actions(id text primary key, org_id text, social_account_id text, evidence_file_id text);
|
||||
create table social_audit_records(id text primary key, org_id text, social_account_id text, evidence_file_id text);
|
||||
create table scheduled_posts(id text primary key, org_id text, social_account_id text, approval_id text, created_by_employee_id text);
|
||||
create table policy_rules(id text primary key, org_id text);
|
||||
create table compliance_exports(id text primary key, org_id text, requested_by_employee_id text);
|
||||
@@ -218,6 +219,7 @@ describe("migration service", () => {
|
||||
insert into browser_action_policies(id, org_id, social_account_id) values ('browser-policy-1', 'org-1', 'social-1');
|
||||
insert into social_approval_routes(id, org_id, social_account_id, route_to_id) values ('social-route-1', 'org-1', 'social-1', 'emp-1');
|
||||
insert into browser_actions(id, org_id, social_account_id, evidence_file_id) values ('browser-1', 'org-1', 'social-1', 'file-1');
|
||||
insert into social_audit_records(id, org_id, social_account_id, evidence_file_id) values ('social-audit-1', 'org-1', 'social-1', 'file-1');
|
||||
insert into scheduled_posts(id, org_id, social_account_id, approval_id, created_by_employee_id) values ('post-1', 'org-1', 'social-1', 'approval-1', 'emp-1');
|
||||
insert into policy_rules(id, org_id) values ('policy-1', 'org-1');
|
||||
insert into compliance_exports(id, org_id, requested_by_employee_id) values ('cmp-1', 'org-1', 'emp-1');
|
||||
@@ -256,6 +258,7 @@ describe("migration service", () => {
|
||||
expect(body.tableCounts.browser_session_containers).toBe(1);
|
||||
expect(body.tableCounts.browser_action_policies).toBe(1);
|
||||
expect(body.tableCounts.social_approval_routes).toBe(1);
|
||||
expect(body.tableCounts.social_audit_records).toBe(1);
|
||||
expect(body.tableCounts.compliance_exports).toBe(1);
|
||||
expect(body.tableCounts.audit_replay_runs).toBe(1);
|
||||
expect(body.tableCounts.rollback_snapshots).toBe(1);
|
||||
@@ -278,6 +281,7 @@ describe("migration service", () => {
|
||||
expect(body.idMaps.browser_session_containers).toEqual(["container-1"]);
|
||||
expect(body.idMaps.browser_action_policies).toEqual(["browser-policy-1"]);
|
||||
expect(body.idMaps.social_approval_routes).toEqual(["social-route-1"]);
|
||||
expect(body.idMaps.social_audit_records).toEqual(["social-audit-1"]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,14 @@ describe("social service", () => {
|
||||
approvalKind: "social_publish",
|
||||
routeToId: employeeId,
|
||||
});
|
||||
expect(state.socialAuditRecords[0]).toMatchObject({
|
||||
socialAccountId: state.socialAccounts[0]?.id,
|
||||
sourceType: "social_account",
|
||||
eventType: "account_created",
|
||||
actorType: "employee",
|
||||
actorId: employeeId,
|
||||
policyDecision: "allowed_employee_authorization",
|
||||
});
|
||||
expect(state.auditEvents.find((event) => event.entityType === "social_account")?.actorId).toBe(employeeId);
|
||||
expect(grantScope.actions).toEqual(["login", "draft", "approve"]);
|
||||
expect(grantScope.riskCeiling).toBe("medium");
|
||||
@@ -153,6 +161,47 @@ describe("social service", () => {
|
||||
expect(state.scheduledPosts[0]?.state).toBe("published");
|
||||
expect(state.scheduledPosts[0]?.socialAccountId).toBe(socialAccountId);
|
||||
expect(state.scheduledPosts[0]?.approvalId).toBe(state.approvals[0]?.id);
|
||||
expect(state.socialAuditRecords.map((record) => record.eventType).sort()).toEqual([
|
||||
"account_created",
|
||||
"draft_created",
|
||||
"state_published",
|
||||
"state_scheduled",
|
||||
]);
|
||||
expect(state.socialAuditRecords.find((record) => record.eventType === "state_scheduled")).toMatchObject({
|
||||
sourceId: postId,
|
||||
policyDecision: "approval_requested",
|
||||
});
|
||||
expect(state.socialAuditRecords.find((record) => record.eventType === "state_published")).toMatchObject({
|
||||
sourceId: postId,
|
||||
policyDecision: "allowed_after_approval",
|
||||
});
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("keeps social audit records immutable", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const social = new SocialService(connection.db);
|
||||
let auditId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
const orgId = createOrganization(organization);
|
||||
const employeeId = firstEmployeeId(connection, orgId);
|
||||
social.createAccount({
|
||||
orgId,
|
||||
platform: SOCIAL_PLATFORM.X,
|
||||
handle: "@acme",
|
||||
displayName: "Acme",
|
||||
createdByEmployeeId: employeeId,
|
||||
authorizedEmployeeId: employeeId,
|
||||
});
|
||||
auditId = (connection.db.prepare("select id from social_audit_records limit 1").get() as { id: string }).id;
|
||||
});
|
||||
|
||||
expect(() => connection.db.prepare("update social_audit_records set policy_decision = ? where id = ?")
|
||||
.run("changed", auditId)).toThrow("Social audit records are immutable.");
|
||||
expect(() => connection.db.prepare("delete from social_audit_records where id = ?")
|
||||
.run(auditId)).toThrow("Social audit records are immutable.");
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user