#include "core/research_store.h" #include #include #define INVESTIGATION_ID "82000000-0000-4000-8000-000000000001" #define OTHER_INVESTIGATION_ID "82000000-0000-4000-8000-000000000002" #define PLAN_ID "82000000-0000-4000-8000-000000000010" #define ACTION_DNS_ID "82000000-0000-4000-8000-000000000011" #define ACTION_HTTP_ID "82000000-0000-4000-8000-000000000012" #define GRANT_ID "82000000-0000-4000-8000-000000000020" #define EXPIRED_GRANT_ID "82000000-0000-4000-8000-000000000021" #define BAD_GRANT_ID "82000000-0000-4000-8000-000000000022" #define WAVE_GRANT_ID "82000000-0000-4000-8000-000000000023" #define REFUSE_GRANT_ID "82000000-0000-4000-8000-000000000024" #define CAMPAIGN_ID "82000000-0000-4000-8000-000000000030" #define RESULT_ID "82000000-0000-4000-8000-000000000031" #define RECEIPT_ID "82000000-0000-4000-8000-000000000032" #define HASH_A "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" #define HASH_B "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" #define HASH_C "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" #define HASH_D "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" static const ResearchActionDecision grant_decisions[] = { {ACTION_DNS_ID, RESEARCH_ACTION_AUTHORIZE}, {ACTION_HTTP_ID, RESEARCH_ACTION_AUTHORIZE}}; static const char *const decision_time = "2026-09-27T10:01:00Z"; typedef struct { char *directory; char *path; LocalJobStore *store; } Fixture; static void setup(Fixture *fixture, gconstpointer unused) { (void)unused; GError *error = NULL; fixture->directory = g_dir_make_tmp("labfy-research-XXXXXX", &error); g_assert_no_error(error); fixture->path = g_build_filename(fixture->directory, "jobs.sqlite", NULL); fixture->store = local_job_store_create(fixture->path, INVESTIGATION_ID, &error); g_assert_no_error(error); g_assert_nonnull(fixture->store); } static void teardown(Fixture *fixture, gconstpointer unused) { (void)unused; local_job_store_close(fixture->store); g_remove(fixture->path); g_rmdir(fixture->directory); g_free(fixture->path); g_free(fixture->directory); } static ResearchPlan plan_fixture(ResearchAction actions[2], ResearchSeed seeds[2]) { seeds[0] = (ResearchSeed){RESEARCH_SEED_DOMAIN, "specimen.example"}; seeds[1] = (ResearchSeed){RESEARCH_SEED_SEARCH_TERM, "SPECIMEN ALPHA"}; actions[0] = (ResearchAction){ .action_id = ACTION_DNS_ID, .capability_id = "research.dns.passive", .provider_id = "specimen.provider", .endpoint = "https://api.example.test/dns", .subject = "specimen.example", .contact = RESEARCH_CONTACT_THIRD_PARTY, .disclosure = "DOMAIN_TO_PROVIDER", .max_requests = 2, .max_response_bytes = 4096, .max_active_ms = 500}; actions[1] = (ResearchAction){ .action_id = ACTION_HTTP_ID, .capability_id = "research.http.target", .provider_id = "direct", .endpoint = "https://specimen.example/profile", .subject = "https://specimen.example/profile", .contact = RESEARCH_CONTACT_TARGET, .disclosure = "SOURCE_IP_TO_TARGET", .max_requests = 1, .max_response_bytes = 8192, .max_active_ms = 700}; return (ResearchPlan){ .contract = RESEARCH_PLAN_CONTRACT, .plan_id = PLAN_ID, .idempotency_key = "plan-key-specimen", .content_fingerprint = HASH_A, .input_fingerprint = HASH_B, .input_revision = 3, .created_at = "2026-09-27T10:00:00Z", .seeds = seeds, .seed_count = 2, .actions = actions, .action_count = 2}; } static ScopeGrant grant_fixture(const char *grant_id, const char *key, const char *fingerprint, const char *expires_at, const char *const *selected, gsize selected_count) { static const char *const exclusions[] = { "https://specimen.example/profile"}; return (ScopeGrant){ .contract = RESEARCH_GRANT_CONTRACT, .grant_id = grant_id, .investigation_id = INVESTIGATION_ID, .idempotency_key = key, .content_fingerprint = fingerprint, .plan_fingerprint = HASH_A, .created_at = "2026-09-27T10:01:00Z", .expires_at = expires_at, .selected_action_ids = selected, .selected_action_count = selected_count, .excluded_subjects = exclusions, .excluded_subject_count = G_N_ELEMENTS(exclusions), .max_requests = 3, .max_response_bytes = 12288, .max_active_ms = 1200}; } static gint64 table_count(const char *path, const char *table) { sqlite3 *db = NULL; sqlite3_stmt *statement = NULL; g_assert_cmpint(sqlite3_open(path, &db), ==, SQLITE_OK); char *sql = g_strdup_printf("SELECT count(*) FROM %s;", table); g_assert_cmpint(sqlite3_prepare_v2(db, sql, -1, &statement, NULL), ==, SQLITE_OK); g_assert_cmpint(sqlite3_step(statement), ==, SQLITE_ROW); gint64 value = sqlite3_column_int64(statement, 0); sqlite3_finalize(statement); sqlite3_close(db); g_free(sql); return value; } static void admit_valid_plan(Fixture *fixture, ResearchAction actions[2], ResearchSeed seeds[2]) { GError *error = NULL; ResearchPlan plan = plan_fixture(actions, seeds); gboolean reused = TRUE; g_assert_true( research_store_admit_plan(fixture->store, &plan, &reused, &error)); g_assert_no_error(error); g_assert_false(reused); } static void test_plan_atomic_idempotence(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; ResearchPlan plan = plan_fixture(actions, seeds); actions[1].max_requests = 0; GError *error = NULL; g_assert_false(research_store_admit_plan(fixture->store, &plan, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "research_plans"), ==, 0); g_assert_cmpint(table_count(fixture->path, "research_actions"), ==, 0); plan = plan_fixture(actions, seeds); gboolean reused = FALSE; g_assert_true( research_store_admit_plan(fixture->store, &plan, &reused, &error)); g_assert_false(reused); g_assert_true( research_store_admit_plan(fixture->store, &plan, &reused, &error)); g_assert_true(reused); plan.content_fingerprint = HASH_C; g_assert_false( research_store_admit_plan(fixture->store, &plan, &reused, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "research_plans"), ==, 1); g_assert_cmpint(table_count(fixture->path, "research_actions"), ==, 2); } static ResearchPolicyDecision decide(Fixture *fixture, const char *grant_id, const ResearchAction *action, const char *investigation_id, const char *plan_fingerprint, const char *now) { ResearchPolicyRequest request = { .investigation_id = investigation_id, .plan_fingerprint = plan_fingerprint, .action = action, .now = now, .requested_requests = 1, .requested_response_bytes = 1024, .requested_active_ms = 100}; ResearchPolicyDecision decision = RESEARCH_POLICY_DENY_ACTION; GError *error = NULL; g_assert_true(research_store_policy_decide(fixture->store, grant_id, &request, &decision, &error)); g_assert_no_error(error); return decision; } static void test_grant_policy(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const selected[] = {ACTION_DNS_ID}; ScopeGrant grant = grant_fixture(GRANT_ID, "grant-key-specimen", HASH_C, "2026-09-28T00:00:00Z", selected, G_N_ELEMENTS(selected)); GError *error = NULL; gboolean reused = FALSE; g_assert_true( research_store_admit_grant(fixture->store, &grant, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, &reused, &error)); g_assert_false(reused); g_assert_true( research_store_admit_grant(fixture->store, &grant, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, &reused, &error)); g_assert_true(reused); grant.content_fingerprint = HASH_D; g_assert_false( research_store_admit_grant(fixture->store, &grant, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, &reused, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS); g_clear_error(&error); g_assert_cmpint(decide(fixture, GRANT_ID, &actions[0], INVESTIGATION_ID, HASH_A, "2026-09-27T12:00:00Z"), ==, RESEARCH_POLICY_ALLOW); g_assert_cmpint(decide(fixture, GRANT_ID, &actions[0], OTHER_INVESTIGATION_ID, HASH_A, "2026-09-27T12:00:00Z"), ==, RESEARCH_POLICY_DENY_INVESTIGATION); ResearchAction redirected = actions[0]; redirected.endpoint = "https://redirected.example.test/dns"; g_assert_cmpint(decide(fixture, GRANT_ID, &redirected, INVESTIGATION_ID, HASH_A, "2026-09-27T12:00:00Z"), ==, RESEARCH_POLICY_DENY_ACTION); g_assert_cmpint(decide(fixture, GRANT_ID, &actions[1], INVESTIGATION_ID, HASH_A, "2026-09-27T12:00:00Z"), ==, RESEARCH_POLICY_DENY_EXCLUDED); g_assert_true(research_store_revoke_grant( fixture->store, GRANT_ID, "2026-09-27T13:00:00Z", &error)); g_assert_cmpint(decide(fixture, GRANT_ID, &actions[1], INVESTIGATION_ID, HASH_A, "2026-09-27T14:00:00Z"), ==, RESEARCH_POLICY_DENY_EXCLUDED); g_assert_cmpint(decide(fixture, GRANT_ID, &actions[0], INVESTIGATION_ID, HASH_A, "2026-09-27T14:00:00Z"), ==, RESEARCH_POLICY_DENY_REVOKED); } static void test_expiry_and_atomic_bad_action(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const selected[] = {ACTION_DNS_ID}; ScopeGrant expired = grant_fixture(EXPIRED_GRANT_ID, "expired-key", HASH_C, "2026-09-27T11:00:00Z", selected, G_N_ELEMENTS(selected)); GError *error = NULL; g_assert_true(research_store_admit_grant(fixture->store, &expired, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); g_assert_cmpint(decide(fixture, EXPIRED_GRANT_ID, &actions[0], INVESTIGATION_ID, HASH_A, "2026-09-27T11:00:00Z"), ==, RESEARCH_POLICY_DENY_EXPIRED); static const char *const unknown[] = { "82000000-0000-4000-8000-000000000099"}; ScopeGrant bad = grant_fixture(BAD_GRANT_ID, "bad-key", HASH_D, "2026-09-28T00:00:00Z", unknown, G_N_ELEMENTS(unknown)); g_assert_false( research_store_admit_grant(fixture->store, &bad, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_FAILED); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "scope_grants"), ==, 1); g_assert_true(local_job_store_integrity(fixture->store, &error)); g_assert_no_error(error); } static void test_cross_investigation_admission(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const selected[] = {ACTION_DNS_ID}; ScopeGrant grant = grant_fixture(GRANT_ID, "cross-key", HASH_C, "2026-09-28T00:00:00Z", selected, G_N_ELEMENTS(selected)); grant.investigation_id = OTHER_INVESTIGATION_ID; GError *error = NULL; g_assert_false( research_store_admit_grant(fixture->store, &grant, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "scope_grants"), ==, 0); } static void test_grant_decision_coverage(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const selected[] = {ACTION_DNS_ID}; ScopeGrant grant = grant_fixture(GRANT_ID, "coverage-key", HASH_C, "2026-09-28T00:00:00Z", selected, G_N_ELEMENTS(selected)); ResearchActionDecision incomplete[] = { {ACTION_DNS_ID, RESEARCH_ACTION_AUTHORIZE}}; ResearchActionDecision complete[] = { {ACTION_DNS_ID, RESEARCH_ACTION_AUTHORIZE}, {ACTION_HTTP_ID, RESEARCH_ACTION_DEFER}}; GError *error = NULL; g_assert_false(research_store_admit_grant(fixture->store, &grant, incomplete, G_N_ELEMENTS(incomplete), decision_time, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "scope_grants"), ==, 0); g_assert_cmpint(table_count(fixture->path, "research_action_decisions"), ==, 0); g_assert_true(research_store_admit_grant(fixture->store, &grant, complete, G_N_ELEMENTS(complete), decision_time, NULL, &error)); g_assert_no_error(error); g_assert_cmpint(table_count(fixture->path, "scope_grants"), ==, 1); g_assert_cmpint(table_count(fixture->path, "research_action_decisions"), ==, 2); } static void test_refusal_is_terminal(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const first_selected[] = {ACTION_DNS_ID}; ResearchActionDecision refused[] = { {ACTION_DNS_ID, RESEARCH_ACTION_AUTHORIZE}, {ACTION_HTTP_ID, RESEARCH_ACTION_REFUSE}}; ScopeGrant first = grant_fixture(GRANT_ID, "refusal-first", HASH_C, "2026-09-28T00:00:00Z", first_selected, G_N_ELEMENTS(first_selected)); GError *error = NULL; g_assert_true(research_store_admit_grant(fixture->store, &first, refused, G_N_ELEMENTS(refused), decision_time, NULL, &error)); static const char *const refused_selected[] = {ACTION_HTTP_ID}; ResearchActionDecision retry[] = { {ACTION_DNS_ID, RESEARCH_ACTION_AUTHORIZE}, {ACTION_HTTP_ID, RESEARCH_ACTION_AUTHORIZE}}; ScopeGrant second = grant_fixture(REFUSE_GRANT_ID, "refusal-retry", HASH_D, "2026-09-28T00:00:00Z", refused_selected, G_N_ELEMENTS(refused_selected)); g_assert_false(research_store_admit_grant(fixture->store, &second, retry, G_N_ELEMENTS(retry), decision_time, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_FAILED); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "scope_grants"), ==, 1); } static void test_campaign_result_receipt(Fixture *fixture, gconstpointer unused) { (void)unused; ResearchAction actions[2]; ResearchSeed seeds[2]; admit_valid_plan(fixture, actions, seeds); static const char *const wave_two[] = {ACTION_HTTP_ID}; ScopeGrant premature = grant_fixture(WAVE_GRANT_ID, "wave-two-premature", HASH_C, "2026-09-28T00:00:00Z", wave_two, G_N_ELEMENTS(wave_two)); GError *error = NULL; g_assert_false(research_store_admit_grant(fixture->store, &premature, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_FAILED); g_clear_error(&error); static const char *const selected[] = {ACTION_DNS_ID}; ScopeGrant grant = grant_fixture(GRANT_ID, "campaign-grant", HASH_C, "2026-09-28T00:00:00Z", selected, G_N_ELEMENTS(selected)); g_assert_true(research_store_admit_grant(fixture->store, &grant, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); ResearchCampaign campaign = { .contract = RESEARCH_CAMPAIGN_CONTRACT, .campaign_id = CAMPAIGN_ID, .investigation_id = INVESTIGATION_ID, .grant_id = GRANT_ID, .idempotency_key = "campaign-key", .content_fingerprint = HASH_D, .created_at = "2026-09-27T10:02:00Z"}; gboolean reused = FALSE; g_assert_true(research_store_admit_campaign(fixture->store, &campaign, &reused, &error)); g_assert_false(reused); g_assert_true(research_store_admit_campaign(fixture->store, &campaign, &reused, &error)); g_assert_true(reused); ResearchResult result = { .contract = RESEARCH_RESULT_CONTRACT, .result_id = RESULT_ID, .campaign_id = CAMPAIGN_ID, .action_id = ACTION_DNS_ID, .raw_artifact_relative_path = "research/raw/specimen-dns.json", .content_sha256 = HASH_B, .status = "CAPTURED"}; ResearchReceipt receipt = { .contract = RESEARCH_RECEIPT_CONTRACT, .receipt_id = RECEIPT_ID, .result_id = RESULT_ID, .campaign_id = CAMPAIGN_ID, .grant_id = GRANT_ID, .action_id = ACTION_DNS_ID, .decided_at = "2026-09-27T10:03:00Z", .decision_code = "ALLOW"}; result.raw_artifact_relative_path = "../outside.json"; g_assert_false(research_store_record_result(fixture->store, &result, &receipt, &error)); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_clear_error(&error); g_assert_cmpint(table_count(fixture->path, "research_results"), ==, 0); result.raw_artifact_relative_path = "research/raw/specimen-dns.json"; g_assert_true(research_store_record_result(fixture->store, &result, &receipt, &error)); g_assert_cmpint(table_count(fixture->path, "research_results"), ==, 1); g_assert_cmpint(table_count(fixture->path, "research_receipts"), ==, 1); ScopeGrant admitted_wave = grant_fixture(WAVE_GRANT_ID, "wave-two-after-first", HASH_C, "2026-09-28T00:00:00Z", wave_two, G_N_ELEMENTS(wave_two)); g_assert_true(research_store_admit_grant(fixture->store, &admitted_wave, grant_decisions, G_N_ELEMENTS(grant_decisions), decision_time, NULL, &error)); g_assert_true(local_job_store_integrity(fixture->store, &error)); g_assert_no_error(error); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add("/research/plan-atomic-idempotence", Fixture, NULL, setup, test_plan_atomic_idempotence, teardown); g_test_add("/research/grant-policy", Fixture, NULL, setup, test_grant_policy, teardown); g_test_add("/research/expiry-atomic-bad-action", Fixture, NULL, setup, test_expiry_and_atomic_bad_action, teardown); g_test_add("/research/cross-investigation", Fixture, NULL, setup, test_cross_investigation_admission, teardown); g_test_add("/research/grant-decision-coverage", Fixture, NULL, setup, test_grant_decision_coverage, teardown); g_test_add("/research/refusal-terminal", Fixture, NULL, setup, test_refusal_is_terminal, teardown); g_test_add("/research/campaign-result-receipt", Fixture, NULL, setup, test_campaign_result_receipt, teardown); return g_test_run(); }