Monorepo for Tangled

appview/db: add cascade delete test for pipelines and triggers

verifies that deleting a repo removes associated triggers,
pipelines, and pipeline_statuses via on delete cascade

Signed-off-by: moshyfawn <email@moshyfawn.dev>

+180
+180
appview/db/pipeline_test.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "testing" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + _ "github.com/mattn/go-sqlite3" 9 + "github.com/stretchr/testify/assert" 10 + "github.com/stretchr/testify/require" 11 + "tangled.org/core/appview/models" 12 + "tangled.org/core/orm" 13 + ) 14 + 15 + func setupPipelineTestDB(t *testing.T) *sql.DB { 16 + t.Helper() 17 + db, err := sql.Open("sqlite3", ":memory:?_foreign_keys=1") 18 + require.NoError(t, err) 19 + 20 + _, err = db.Exec(` 21 + create table if not exists repos ( 22 + id integer primary key autoincrement, 23 + did text not null, 24 + name text not null, 25 + rkey text not null default '', 26 + at_uri text not null unique, 27 + knot text not null default '', 28 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 29 + unique(did, name) 30 + ); 31 + 32 + create table if not exists triggers ( 33 + -- primary key 34 + id integer primary key autoincrement, 35 + 36 + -- top-level fields 37 + kind text not null, 38 + repo_at text not null, 39 + 40 + -- pushTriggerData fields 41 + push_ref text, 42 + push_new_sha text check (length(push_new_sha) = 40), 43 + push_old_sha text check (length(push_old_sha) = 40), 44 + 45 + -- pullRequestTriggerData fields 46 + pr_source_branch text, 47 + pr_target_branch text, 48 + pr_source_sha text check (length(pr_source_sha) = 40), 49 + pr_action text, 50 + 51 + foreign key (repo_at) references repos(at_uri) on delete cascade 52 + ); 53 + 54 + create table if not exists pipelines ( 55 + -- identifiers 56 + id integer primary key autoincrement, 57 + knot text not null, 58 + rkey text not null, 59 + 60 + repo_owner text not null, 61 + repo_name text not null, 62 + repo_at text not null, 63 + 64 + -- every pipeline must be associated with exactly one commit 65 + sha text not null check (length(sha) = 40), 66 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 67 + 68 + -- trigger data 69 + trigger_id integer not null, 70 + 71 + unique(knot, rkey), 72 + foreign key (trigger_id) references triggers(id) on delete cascade, 73 + foreign key (repo_at) references repos(at_uri) on delete cascade 74 + ); 75 + 76 + create table if not exists pipeline_statuses ( 77 + -- identifiers 78 + id integer primary key autoincrement, 79 + spindle text not null, 80 + rkey text not null, 81 + 82 + -- referenced pipeline. these form the (did, rkey) pair 83 + pipeline_knot text not null, 84 + pipeline_rkey text not null, 85 + 86 + -- content 87 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 88 + workflow text not null, 89 + status text not null, 90 + error text, 91 + exit_code integer not null default 0, 92 + 93 + unique (spindle, rkey), 94 + foreign key (pipeline_knot, pipeline_rkey) 95 + references pipelines (knot, rkey) 96 + on delete cascade 97 + ); 98 + `) 99 + require.NoError(t, err) 100 + 101 + t.Cleanup(func() { db.Close() }) 102 + return db 103 + } 104 + 105 + func seedRepo(t *testing.T, db *sql.DB, did, name, rkey, knot string) syntax.ATURI { 106 + t.Helper() 107 + atURI := syntax.ATURI("at://" + did + "/sh.tangled.repo/" + rkey) 108 + _, err := db.Exec( 109 + `insert into repos (did, name, rkey, at_uri, knot) values (?, ?, ?, ?, ?)`, 110 + did, name, rkey, string(atURI), knot, 111 + ) 112 + require.NoError(t, err) 113 + return atURI 114 + } 115 + 116 + func TestPipelineCascadeDeleteOnRepoRemoval(t *testing.T) { 117 + d := setupPipelineTestDB(t) 118 + 119 + knot := "example.com" 120 + did := "did:plc:foo" 121 + repoName := "my-repo" 122 + testSha := "0000000000000000000000000000000000000000" 123 + 124 + repoAt := seedRepo(t, d, did, repoName, "abc123", knot) 125 + 126 + triggerID, err := AddTrigger(d, models.Trigger{ 127 + Kind: "push", 128 + RepoAt: repoAt, 129 + }) 130 + require.NoError(t, err) 131 + 132 + err = AddPipeline(d, models.Pipeline{ 133 + Rkey: "pip1", 134 + Knot: knot, 135 + RepoOwner: syntax.DID(did), 136 + RepoName: repoName, 137 + RepoAt: repoAt, 138 + TriggerId: int(triggerID), 139 + Sha: testSha, 140 + }) 141 + require.NoError(t, err) 142 + 143 + // add a pipeline_status referencing the pipeline 144 + _, err = d.Exec( 145 + `insert into pipeline_statuses (spindle, rkey, pipeline_knot, pipeline_rkey, workflow, status, exit_code) 146 + values (?, ?, ?, ?, ?, ?, ?)`, 147 + "spindle.example.com", "status1", knot, "pip1", "ci.yaml", "success", 0, 148 + ) 149 + require.NoError(t, err) 150 + 151 + ps, err := GetPipelines(d, orm.FilterEq("repo_at", repoAt)) 152 + require.NoError(t, err) 153 + assert.Len(t, ps, 1) 154 + assert.Equal(t, repoAt, ps[0].RepoAt) 155 + 156 + // verify the production query path (join + repo_at filter) 157 + joined, err := GetPipelineStatuses(d, 1, orm.FilterEq("p.repo_at", repoAt)) 158 + require.NoError(t, err) 159 + assert.Len(t, joined, 1) 160 + assert.Equal(t, repoAt, joined[0].RepoAt) 161 + 162 + // cascade: removing the repo should delete its pipelines, triggers, 163 + // and pipeline_statuses 164 + _, err = d.Exec(`delete from repos where at_uri = ?`, string(repoAt)) 165 + require.NoError(t, err) 166 + 167 + ps, err = GetPipelines(d) 168 + require.NoError(t, err) 169 + assert.Empty(t, ps) 170 + 171 + var triggerCount int 172 + err = d.QueryRow(`select count(*) from triggers`).Scan(&triggerCount) 173 + require.NoError(t, err) 174 + assert.Equal(t, 0, triggerCount) 175 + 176 + var statusCount int 177 + err = d.QueryRow(`select count(*) from pipeline_statuses`).Scan(&statusCount) 178 + require.NoError(t, err) 179 + assert.Equal(t, 0, statusCount) 180 + }