@@ -22,6 +22,7 @@ import (
2222 "testing"
2323 "time"
2424
25+ "github.com/apache/airavata-custos/connectors/SLURM/Rest-Client/pkg/client"
2526 "github.com/apache/airavata-custos/pkg/models"
2627)
2728
@@ -153,3 +154,116 @@ func TestAllocationDeletionRemovesAllAssociations(t *testing.T) {
153154 t .Fatalf ("expected an account-wide delete on allocation deletion, got %+v" , got )
154155 }
155156}
157+
158+ // The sweep is the backstop for a lost deactivation: an association the
159+ // allocations no longer call for is removed even if no event ever arrived.
160+ func TestReconcilerRemovesStaleAssociation (t * testing.T ) {
161+ core := coreMock (mockOpts {
162+ provisionedAt : ago (time .Minute ),
163+ memberships : []models.ComputeAllocationMembership {testMembership ()},
164+ })
165+ slurm := & fakeSlurmClient {existing : []client.Association {
166+ // desired
167+ {Account : "test-alloc" , Cluster : "testcluster" , User : "testuser" , Partition : "compute" },
168+ // left behind by a membership that is gone
169+ {Account : "test-alloc" , Cluster : "testcluster" , User : "ghost" , Partition : "compute" },
170+ }}
171+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
172+
173+ got := slurm .allDeletes ()
174+ if len (got ) != 1 {
175+ t .Fatalf ("expected the stale association to be removed, got %d deletes" , len (got ))
176+ }
177+ if got [0 ].User != "ghost" || got [0 ].Partition != "compute" {
178+ t .Errorf ("removed the wrong association: %+v" , got [0 ])
179+ }
180+ }
181+
182+ // Guard: an empty desired set almost always means a failed lookup, so the
183+ // sweep must not read it as "revoke everyone".
184+ func TestReconcilerDoesNotPruneWhenNothingIsDesired (t * testing.T ) {
185+ core := coreMock (mockOpts {
186+ provisionedAt : ago (time .Minute ),
187+ memberships : nil , // nobody entitled to anything
188+ })
189+ slurm := & fakeSlurmClient {existing : []client.Association {
190+ {Account : "test-alloc" , Cluster : "testcluster" , User : "testuser" , Partition : "compute" },
191+ }}
192+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
193+
194+ if n := len (slurm .allDeletes ()); n != 0 {
195+ t .Fatalf ("an empty desired set must not revoke anything, got %d deletes" , n )
196+ }
197+ }
198+
199+ // Guard: associations on accounts Custos does not manage are never touched.
200+ func TestReconcilerLeavesUnmanagedAccountsAlone (t * testing.T ) {
201+ core := coreMock (mockOpts {
202+ provisionedAt : ago (time .Minute ),
203+ memberships : []models.ComputeAllocationMembership {testMembership ()},
204+ unmanagedAccounts : true , // core reports no allocations on this cluster
205+ })
206+ slurm := & fakeSlurmClient {existing : []client.Association {
207+ {Account : "someone-elses-account" , Cluster : "testcluster" , User : "outsider" , Partition : "compute" },
208+ }}
209+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
210+
211+ if n := len (slurm .allDeletes ()); n != 0 {
212+ t .Fatalf ("an unmanaged account must not be touched, got %d deletes" , n )
213+ }
214+ }
215+
216+ // Account-level records carry the allocation's own limits, not a member's
217+ // access, so the sweep must leave them be.
218+ func TestReconcilerLeavesAccountLevelAssociationsAlone (t * testing.T ) {
219+ core := coreMock (mockOpts {
220+ provisionedAt : ago (time .Minute ),
221+ memberships : []models.ComputeAllocationMembership {testMembership ()},
222+ })
223+ slurm := & fakeSlurmClient {existing : []client.Association {
224+ {Account : "test-alloc" , Cluster : "testcluster" , User : "testuser" , Partition : "compute" },
225+ {Account : "test-alloc" , Cluster : "testcluster" , User : "" }, // account-level
226+ }}
227+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
228+
229+ if n := len (slurm .allDeletes ()); n != 0 {
230+ t .Fatalf ("account-level associations must not be pruned, got %d deletes" , n )
231+ }
232+ }
233+
234+ // A member inside the provisioning grace is still entitled, so their fresh
235+ // association must not be pruned just because the sweep is not writing it yet.
236+ func TestReconcilerDoesNotPruneAssociationsInsideGrace (t * testing.T ) {
237+ core := coreMock (mockOpts {
238+ provisionedAt : ago (time .Second ),
239+ memberships : []models.ComputeAllocationMembership {testMembership ()},
240+ })
241+ slurm := & fakeSlurmClient {existing : []client.Association {
242+ {Account : "test-alloc" , Cluster : "testcluster" , User : "testuser" , Partition : "compute" },
243+ }}
244+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
245+
246+ if n := len (slurm .allDeletes ()); n != 0 {
247+ t .Fatalf ("a member inside the grace must keep their association, got %d deletes" , n )
248+ }
249+ }
250+
251+ // An allocation that is no longer active grants nothing, so its members'
252+ // associations are swept away even without a deactivation event.
253+ func TestReconcilerRemovesAssociationsForInactiveAllocation (t * testing.T ) {
254+ core := coreMock (mockOpts {
255+ provisionedAt : ago (time .Minute ),
256+ allocationStatus : models .INACTIVE ,
257+ memberships : []models.ComputeAllocationMembership {testMembership ()},
258+ })
259+ slurm := & fakeSlurmClient {existing : []client.Association {
260+ {Account : "test-alloc" , Cluster : "testcluster" , User : "testuser" , Partition : "compute" },
261+ }}
262+ NewAssociationSubscriber (slurm , nil , core , 0 , 0 ).reconcile (context .Background ())
263+
264+ // Desired is empty for an inactive allocation, so the empty-desired guard
265+ // holds and nothing is revoked. The event handler does that job.
266+ if n := len (slurm .allDeletes ()); n != 0 {
267+ t .Fatalf ("expected the empty-desired guard to hold, got %d deletes" , n )
268+ }
269+ }
0 commit comments