Skip to content

Commit abd4749

Browse files
authored
[Exporter] Started to add tests for account-level resources (#5202)
NO_CHANGELOG=true ## Changes <!-- Summary of your changes that are easy to understand --> ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [x] `make test` run locally - [ ] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] using Go SDK - [ ] using TF Plugin Framework - [ ] has entry in `NEXT_CHANGELOG.md` file
1 parent b948d9f commit abd4749

File tree

1 file changed

+339
-0
lines changed

1 file changed

+339
-0
lines changed

exporter/exporter_acctlvl_test.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package exporter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/databricks/databricks-sdk-go/service/catalog"
10+
"github.com/databricks/databricks-sdk-go/service/iam"
11+
"github.com/databricks/terraform-provider-databricks/common"
12+
"github.com/databricks/terraform-provider-databricks/qa"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
// Account-Level Test Helpers and Fixtures
18+
//
19+
// This file contains unit tests for account-level resources in the exporter.
20+
// Account-level resources are different from workspace-level resources in that:
21+
// 1. They require an AccountID to be set in the client configuration
22+
// 2. They use different API endpoints (account-level paths vs workspace paths)
23+
// 3. They use the AccountClient instead of WorkspaceClient
24+
//
25+
// Key patterns for writing account-level tests:
26+
// - Use HTTPFixturesApplyAccount() helper which sets up account client configuration
27+
// - Account API endpoints typically start with /api/2.0/accounts/{account_id}/...
28+
// - Account SCIM endpoints are at /api/2.0/account/scim/v2/... (note: 'account' not 'accounts')
29+
// - Use setClientsForTests() to automatically configure the correct client type
30+
//
31+
// Example HTTP fixtures for account-level APIs:
32+
// - GET /api/2.0/accounts/{account_id}/ruleSets/default - access control rule sets
33+
// - GET /api/2.0/account/scim/v2/Users - account-level users
34+
// - GET /api/2.0/account/scim/v2/Groups - account-level groups
35+
// - GET /api/2.0/account/scim/v2/ServicePrincipals - account-level service principals
36+
37+
const testAccountID = "00000000-0000-0000-0000-000000000000"
38+
39+
// HTTPFixturesApplyAccount is a helper that sets up an account-level client for testing
40+
func HTTPFixturesApplyAccount(t *testing.T, fixtures []qa.HTTPFixture, testFunc func(ctx context.Context, client *common.DatabricksClient)) {
41+
qa.HTTPFixturesApply(t, fixtures, func(ctx context.Context, client *common.DatabricksClient) {
42+
// Configure the client as an account-level client
43+
// Setting AccountID is enough since the client is in testing mode (isTesting=true)
44+
client.Config.AccountID = testAccountID
45+
// Set Host to accounts.cloud.databricks.com to enable AWS detection via IsAws()
46+
client.Config.Host = "https://accounts.cloud.databricks.com"
47+
testFunc(ctx, client)
48+
})
49+
}
50+
51+
// Common fixtures for account-level testing
52+
53+
var emptyAccountUsers = qa.HTTPFixture{
54+
Method: "GET",
55+
Resource: "/api/2.0/account/scim/v2/Users?attributes=id%2CuserName&count=10000&startIndex=1",
56+
Response: iam.ListUsersResponse{},
57+
ReuseRequest: true,
58+
}
59+
60+
var emptyAccountServicePrincipals = qa.HTTPFixture{
61+
Method: "GET",
62+
Resource: "/api/2.0/account/scim/v2/ServicePrincipals?attributes=id%2CuserName&count=10000&startIndex=1",
63+
Response: iam.ListServicePrincipalResponse{},
64+
ReuseRequest: true,
65+
}
66+
67+
var emptyAccountGroups = qa.HTTPFixture{
68+
Method: "GET",
69+
Resource: "/api/2.0/account/scim/v2/Groups?attributes=id%2CdisplayName&count=10000&startIndex=1",
70+
Response: iam.ListGroupsResponse{},
71+
ReuseRequest: true,
72+
}
73+
74+
// TestImportingAccessControlRuleSet tests the import of access control rule sets
75+
func TestImportingAccessControlRuleSet(t *testing.T) {
76+
accountUserFixtures := qa.ListUsersFixtures([]iam.User{
77+
{
78+
Id: "user-123",
79+
UserName: "[email protected]",
80+
},
81+
})
82+
accountGroupFixtures := qa.ListGroupsFixtures([]iam.Group{
83+
{
84+
Id: "group-456",
85+
DisplayName: "TestGroup",
86+
},
87+
})
88+
accountSpFixtures := qa.ListServicePrincipalsFixtures([]iam.ServicePrincipal{
89+
{
90+
Id: "sp-789",
91+
ApplicationId: "12345678-1234-1234-1234-123456789012",
92+
},
93+
})
94+
95+
HTTPFixturesApplyAccount(t,
96+
[]qa.HTTPFixture{
97+
// Account-level user fixtures
98+
accountUserFixtures[0],
99+
accountUserFixtures[1],
100+
{
101+
Method: "GET",
102+
Resource: "/api/2.0/account/scim/v2/Users/user-123?attributes=id,userName,displayName,active,externalId,entitlements,groups,roles",
103+
Response: iam.User{
104+
Id: "user-123",
105+
UserName: "[email protected]",
106+
},
107+
ReuseRequest: true,
108+
},
109+
// Account-level group fixtures
110+
accountGroupFixtures[0],
111+
accountGroupFixtures[1],
112+
{
113+
Method: "GET",
114+
Resource: "/api/2.0/account/scim/v2/Groups/group-456?attributes=id,displayName,active,externalId,entitlements,groups,roles,members,meta",
115+
Response: iam.Group{
116+
Id: "group-456",
117+
DisplayName: "TestGroup",
118+
},
119+
ReuseRequest: true,
120+
},
121+
{
122+
Method: "GET",
123+
Resource: "/api/2.0/account/scim/v2/Groups/group-456?attributes=displayName,externalId,entitlements",
124+
Response: iam.Group{
125+
Id: "group-456",
126+
DisplayName: "TestGroup",
127+
},
128+
ReuseRequest: true,
129+
},
130+
// Account-level service principal fixtures
131+
accountSpFixtures[0],
132+
accountSpFixtures[1],
133+
{
134+
Method: "GET",
135+
Resource: "/api/2.0/account/scim/v2/ServicePrincipals/sp-789?attributes=userName,displayName,active,externalId,entitlements",
136+
Response: iam.ServicePrincipal{
137+
Id: "sp-789",
138+
ApplicationId: "12345678-1234-1234-1234-123456789012",
139+
},
140+
ReuseRequest: true,
141+
},
142+
{
143+
Method: "GET",
144+
Resource: "/api/2.0/account/scim/v2/ServicePrincipals/sp-789?attributes=userName,displayName,active,externalId,entitlements,groups,roles",
145+
Response: iam.ServicePrincipal{
146+
Id: "sp-789",
147+
ApplicationId: "12345678-1234-1234-1234-123456789012",
148+
},
149+
ReuseRequest: true,
150+
},
151+
// Rule set fixture - this is the main resource being tested
152+
{
153+
Method: "GET",
154+
Resource: fmt.Sprintf("/api/2.0/accounts/%s/ruleSets/default?", testAccountID),
155+
Response: iam.RuleSetResponse{
156+
Name: fmt.Sprintf("accounts/%s/ruleSets/default", testAccountID),
157+
Etag: "test-etag-123",
158+
GrantRules: []iam.GrantRule{
159+
{
160+
Principals: []string{
161+
162+
"groups/TestGroup",
163+
"servicePrincipals/12345678-1234-1234-1234-123456789012",
164+
},
165+
Role: "roles/account.admin",
166+
},
167+
},
168+
},
169+
ReuseRequest: true,
170+
},
171+
}, func(ctx context.Context, client *common.DatabricksClient) {
172+
ic := importContextForTestWithClient(ctx, client)
173+
ic.enableServices("access,users,groups")
174+
175+
// Test the List function - it should emit the default rule set
176+
err := ic.Importables["databricks_access_control_rule_set"].List(ic)
177+
assert.NoError(t, err)
178+
179+
// In test mode, resources are added to testEmits instead of being processed
180+
assert.True(t, len(ic.testEmits) > 0, "Expected at least one resource to be emitted")
181+
182+
// Check that the access control rule set was emitted
183+
expectedRuleSetID := fmt.Sprintf("accounts/%s/ruleSets/default", testAccountID)
184+
ruleSetEmitted := false
185+
for key := range ic.testEmits {
186+
if strings.Contains(key, "databricks_access_control_rule_set") && strings.Contains(key, expectedRuleSetID) {
187+
ruleSetEmitted = true
188+
break
189+
}
190+
}
191+
assert.True(t, ruleSetEmitted, "Expected databricks_access_control_rule_set to be emitted")
192+
193+
// Now test the Import function by creating a resource manually and calling Import
194+
// First, we need to create a resource data object with the rule set data
195+
ruleSetResource := ic.Resources["databricks_access_control_rule_set"]
196+
d := ruleSetResource.TestResourceData()
197+
d.SetId(expectedRuleSetID)
198+
d.Set("name", expectedRuleSetID)
199+
d.Set("etag", "test-etag-123")
200+
d.Set("grant_rules", []interface{}{
201+
map[string]interface{}{
202+
"principals": []interface{}{
203+
204+
"groups/TestGroup",
205+
"servicePrincipals/12345678-1234-1234-1234-123456789012",
206+
},
207+
"role": "roles/account.admin",
208+
},
209+
})
210+
211+
// Clear testEmits to track only Import emissions
212+
ic.testEmits = map[string]bool{}
213+
214+
// Call the Import function
215+
err = ic.Importables["databricks_access_control_rule_set"].Import(ic, &resource{
216+
Resource: "databricks_access_control_rule_set",
217+
ID: expectedRuleSetID,
218+
Data: d,
219+
})
220+
assert.NoError(t, err)
221+
222+
// Verify that dependent resources were emitted
223+
assert.True(t, len(ic.testEmits) > 0, "Expected Import to emit dependent resources")
224+
225+
// Check for emitted users, groups, and service principals
226+
userEmitted := false
227+
groupEmitted := false
228+
spEmitted := false
229+
for key := range ic.testEmits {
230+
if strings.Contains(key, "databricks_user") {
231+
userEmitted = true
232+
}
233+
if strings.Contains(key, "databricks_group") {
234+
groupEmitted = true
235+
}
236+
if strings.Contains(key, "databricks_service_principal") {
237+
spEmitted = true
238+
}
239+
}
240+
241+
assert.True(t, userEmitted, "Expected databricks_user to be emitted")
242+
assert.True(t, groupEmitted, "Expected databricks_group to be emitted")
243+
assert.True(t, spEmitted, "Expected databricks_service_principal to be emitted")
244+
})
245+
}
246+
247+
// TestImportingAccessControlRuleSetWithoutGrantRules tests that rule sets without grant rules are ignored
248+
func TestImportingAccessControlRuleSetWithoutGrantRules(t *testing.T) {
249+
HTTPFixturesApplyAccount(t,
250+
[]qa.HTTPFixture{
251+
emptyAccountUsers,
252+
emptyAccountServicePrincipals,
253+
emptyAccountGroups,
254+
{
255+
Method: "GET",
256+
Resource: fmt.Sprintf("/api/2.0/accounts/%s/ruleSets/default?", testAccountID),
257+
Response: iam.RuleSetResponse{
258+
Name: fmt.Sprintf("accounts/%s/ruleSets/default", testAccountID),
259+
Etag: "test-etag-123",
260+
GrantRules: []iam.GrantRule{}, // Empty grant rules
261+
},
262+
ReuseRequest: true,
263+
},
264+
}, func(ctx context.Context, client *common.DatabricksClient) {
265+
ic := importContextForTestWithClient(ctx, client)
266+
ic.enableServices("access")
267+
268+
// List should emit the resource
269+
err := ic.Importables["databricks_access_control_rule_set"].List(ic)
270+
assert.NoError(t, err)
271+
272+
// Verify the resource was emitted
273+
assert.True(t, len(ic.testEmits) > 0, "Expected resource to be emitted")
274+
275+
// Create a resource data object with empty grant rules
276+
expectedRuleSetID := fmt.Sprintf("accounts/%s/ruleSets/default", testAccountID)
277+
ruleSetResource := ic.Resources["databricks_access_control_rule_set"]
278+
d := ruleSetResource.TestResourceData()
279+
d.SetId(expectedRuleSetID)
280+
d.Set("name", expectedRuleSetID)
281+
d.Set("etag", "test-etag-123")
282+
d.Set("grant_rules", []interface{}{}) // Empty grant rules
283+
284+
r := &resource{
285+
Resource: "databricks_access_control_rule_set",
286+
ID: expectedRuleSetID,
287+
Data: d,
288+
}
289+
290+
// Test the Ignore function
291+
importable := ic.Importables["databricks_access_control_rule_set"]
292+
shouldIgnore := importable.Ignore(ic, r)
293+
assert.True(t, shouldIgnore, "Resource with empty grant rules should be ignored")
294+
295+
// Verify the resource was added to ignored list by the Ignore function
296+
assert.Contains(t, ic.ignoredResources, fmt.Sprintf("databricks_access_control_rule_set. ID=%s", expectedRuleSetID))
297+
})
298+
}
299+
300+
// TestImportingMetastoreAssignment tests the metastore assignment resource
301+
func TestImportingMetastoreAssignment(t *testing.T) {
302+
HTTPFixturesApplyAccount(t,
303+
[]qa.HTTPFixture{
304+
{
305+
Method: "GET",
306+
Resource: "/api/2.1/unity-catalog/current-metastore-assignment?workspaceId=12345",
307+
Response: catalog.MetastoreAssignment{
308+
MetastoreId: "metastore-123",
309+
WorkspaceId: 12345,
310+
DefaultCatalogName: "main",
311+
},
312+
ReuseRequest: true,
313+
},
314+
}, func(ctx context.Context, client *common.DatabricksClient) {
315+
ic := importContextForTest()
316+
ic.Client = client
317+
ic.Context = ctx
318+
ic.accountClient, _ = client.AccountClient()
319+
ic.enableServices("uc-metastores")
320+
321+
// Simulate emitting a metastore assignment resource (as would be done by Import)
322+
ic.Emit(&resource{
323+
Resource: "databricks_metastore_assignment",
324+
ID: "12345|metastore-123",
325+
})
326+
327+
// Verify the resource was emitted
328+
assert.True(t, len(ic.testEmits) > 0, "Expected resource to be emitted")
329+
330+
assignmentEmitted := false
331+
for key := range ic.testEmits {
332+
if strings.Contains(key, "databricks_metastore_assignment") && strings.Contains(key, "12345") {
333+
assignmentEmitted = true
334+
break
335+
}
336+
}
337+
assert.True(t, assignmentEmitted, "Expected databricks_metastore_assignment to be emitted")
338+
})
339+
}

0 commit comments

Comments
 (0)