11package integration
22
33import (
4+ "context"
45 "fmt"
6+ "maps"
57 "regexp"
68 "testing"
79
8- op "github.com/1Password/connect-sdk-go/onepassword "
10+ "github.com/google/uuid "
911 "github.com/hashicorp/terraform-plugin-testing/helper/resource"
1012 "github.com/hashicorp/terraform-plugin-testing/terraform"
1113
14+ "github.com/1Password/terraform-provider-onepassword/v2/internal/onepassword/model"
1215 tfconfig "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/terraform/config"
16+ "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/attributes"
17+ "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/checks"
18+ "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/client"
19+ "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/sections"
1320 "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/ssh"
21+ uuidutil "github.com/1Password/terraform-provider-onepassword/v2/test/e2e/utils/uuid"
1422)
1523
1624const testVaultID = "bbucuyq2nn4fozygwttxwizpcy"
@@ -27,8 +35,8 @@ type testItem struct {
2735 Attrs map [string ]string
2836}
2937
30- var testItems = map [op .ItemCategory ]testItem {
31- op .Login : {
38+ var testItems = map [model .ItemCategory ]testItem {
39+ model .Login : {
3240 Title : "Test Login" ,
3341 UUID : "5axoqbjhbx3u7wqmersrg6qnqy" ,
3442 Attrs : map [string ]string {
@@ -38,15 +46,15 @@ var testItems = map[op.ItemCategory]testItem{
3846 "url" : "www.example.com" ,
3947 },
4048 },
41- op .Password : {
49+ model .Password : {
4250 Title : "Test Password" ,
4351 UUID : "axoqeauq7ilndgdpimb4j4dwhi" ,
4452 Attrs : map [string ]string {
4553 "category" : "password" ,
4654 "password" : "testPassword" ,
4755 },
4856 },
49- op .Database : {
57+ model .Database : {
5058 Title : "Test Database" ,
5159 UUID : "ck6mbmf3yjps6gk5qldnx4frni" ,
5260 Attrs : map [string ]string {
@@ -58,15 +66,15 @@ var testItems = map[op.ItemCategory]testItem{
5866 "type" : "mysql" ,
5967 },
6068 },
61- op .SecureNote : {
69+ model .SecureNote : {
6270 Title : "Test Secure Note" ,
6371 UUID : "5xbca3eblv5kxkszrbuhdame4a" ,
6472 Attrs : map [string ]string {
6573 "category" : "secure_note" ,
6674 "note_value" : "This is a test secure note for terraform-provider-onepassword" ,
6775 },
6876 },
69- op .Document : {
77+ model .Document : {
7078 Title : "Test Document" ,
7179 UUID : "p6uyugpmxo6zcxo5fdfctet7xa" ,
7280 Attrs : map [string ]string {
@@ -76,7 +84,7 @@ var testItems = map[op.ItemCategory]testItem{
7684 "file.0.content_base64" : "VGhpcyBpcyBhIHRlc3Q=" ,
7785 },
7886 },
79- op .SSHKey : {
87+ model .SSHKey : {
8088 Title : "Test SSH Key" ,
8189 UUID : "5dbnxvhcknslz4mcaz7lobzt6i" ,
8290 Attrs : map [string ]string {
@@ -100,15 +108,15 @@ func TestAccItemDataSource(t *testing.T) {
100108 }
101109
102110 itemTypes := []struct {
103- category op .ItemCategory
111+ category model .ItemCategory
104112 name string
105113 }{
106- {op .Login , "Login" },
107- {op .Password , "Password" },
108- {op .Database , "Database" },
109- {op .SecureNote , "SecureNote" },
110- {op .Document , "Document" },
111- {op .SSHKey , "SSHKey" },
114+ {model .Login , "Login" },
115+ {model .Password , "Password" },
116+ {model .Database , "Database" },
117+ {model .SecureNote , "SecureNote" },
118+ {model .Document , "Document" },
119+ {model .SSHKey , "SSHKey" },
112120 }
113121
114122 var testCases []itemDataSourceTestCase
@@ -207,3 +215,216 @@ func TestAccItemDataSource_NotFound(t *testing.T) {
207215 })
208216 }
209217}
218+
219+ func TestAccItemDataSource_DetectManualChanges (t * testing.T ) {
220+ // Generate unique identifier for this test run to avoid conflicts in parallel execution
221+ uniqueID := uuid .New ().String ()
222+ var itemUUID string
223+
224+ item := testItemsToCreate [model .Login ]
225+ initialAttrs := maps .Clone (item .Attrs )
226+ initialAttrs ["title" ] = addUniqueIDToTitle (initialAttrs ["title" ].(string ), uniqueID )
227+ initialAttrs ["section" ] = sections .MapSections ([]sections.TestSection {
228+ {
229+ Label : "Original Section" ,
230+ Fields : []sections.TestField {
231+ {Label : "Original Field 1" , Value : "original value 1" , Type : "STRING" },
232+ {Label : "Original Field 2" , Value : "original value 2" , Type : "EMAIL" },
233+ },
234+ },
235+ })
236+
237+ updatedAttrs := maps .Clone (testItemsUpdatedAttrs [model .Login ])
238+ updatedAttrs ["title" ] = initialAttrs ["title" ]
239+ updatedAttrs ["section" ] = sections .MapSections ([]sections.TestSection {
240+ {
241+ Label : "Updated Section" ,
242+ Fields : []sections.TestField {
243+ {Label : "New Field" , Value : "new value" , Type : "URL" },
244+ },
245+ },
246+ })
247+
248+ removedAttrs := map [string ]any {
249+ "title" : initialAttrs ["title" ],
250+ "category" : "login" ,
251+ "url" : []string {},
252+ "tags" : []string {},
253+ "section" : []map [string ]any {},
254+ }
255+
256+ // Initial data source read checks
257+ initialReadChecks := []resource.TestCheckFunc {
258+ logStep (t , "INITIAL_READ" ),
259+ uuidutil .CaptureItemUUID (t , "data.onepassword_item.test_item" , & itemUUID ),
260+ }
261+ bcInitial := checks .BuildItemChecks ("data.onepassword_item.test_item" , initialAttrs )
262+ initialReadChecks = append (initialReadChecks , bcInitial ... )
263+
264+ // Build check function to manually update the item
265+ updateItemOutsideTerraform := func () resource.TestCheckFunc {
266+ return func (s * terraform.State ) error {
267+ t .Log ("MANUALLY_UPDATE_ITEM" )
268+ ctx := context .Background ()
269+
270+ client , err := client .CreateTestClient (ctx )
271+ if err != nil {
272+ return fmt .Errorf ("failed to create client: %w" , err )
273+ }
274+
275+ currentItem := & model.Item {
276+ ID : itemUUID ,
277+ VaultID : testVaultID ,
278+ Category : model .Login ,
279+ }
280+ updatedItem := attributes .BuildUpdatedItemAttrs (currentItem , updatedAttrs )
281+
282+ _ , err = client .UpdateItem (ctx , updatedItem , testVaultID )
283+ if err != nil {
284+ return fmt .Errorf ("failed to update item: %w" , err )
285+ }
286+
287+ return nil
288+ }
289+ }
290+
291+ // Build checks for updated data source read
292+ updatedReadChecks := []resource.TestCheckFunc {
293+ logStep (t , "READ_AFTER_UPDATE" ),
294+ }
295+ bcUpdated := checks .BuildItemChecks ("data.onepassword_item.test_item" , updatedAttrs )
296+ updatedReadChecks = append (updatedReadChecks , bcUpdated ... )
297+
298+ // Build check function to manually remove all fields
299+ removeFieldsOutsideTerraform := func () resource.TestCheckFunc {
300+ return func (s * terraform.State ) error {
301+ t .Log ("MANUALLY_REMOVE_ALL_FIELDS" )
302+ ctx := context .Background ()
303+
304+ client , err := client .CreateTestClient (ctx )
305+ if err != nil {
306+ return fmt .Errorf ("failed to create client: %w" , err )
307+ }
308+
309+ strippedItem := & model.Item {
310+ ID : itemUUID ,
311+ Title : removedAttrs ["title" ].(string ),
312+ VaultID : testVaultID ,
313+ Category : model .Login ,
314+ Tags : []string {},
315+ URLs : []model.ItemURL {
316+ {URL : "" , Primary : true },
317+ },
318+ Sections : []model.ItemSection {},
319+ Fields : []model.ItemField {},
320+ }
321+
322+ _ , err = client .UpdateItem (ctx , strippedItem , testVaultID )
323+ if err != nil {
324+ return fmt .Errorf ("failed to remove fields: %w" , err )
325+ }
326+
327+ return nil
328+ }
329+ }
330+
331+ // Build checks for reading after field removal
332+ removedFieldsReadChecks := []resource.TestCheckFunc {
333+ logStep (t , "READ_AFTER_REMOVAL" ),
334+ }
335+ bcRemoved := checks .BuildItemChecks ("data.onepassword_item.test_item" , removedAttrs )
336+ removedFieldsReadChecks = append (removedFieldsReadChecks , bcRemoved ... )
337+
338+ // Verify that username is either not present (SDK) or empty (Connect)
339+ removedFieldsReadChecks = append (removedFieldsReadChecks , resource .TestCheckFunc (func (s * terraform.State ) error {
340+ item , ok := s .RootModule ().Resources ["data.onepassword_item.test_item" ]
341+ if ! ok {
342+ return fmt .Errorf ("resource not found in state" )
343+ }
344+
345+ username , exists := item .Primary .Attributes ["username" ]
346+ if exists {
347+ // If username exists, it should be empty (Connect behavior)
348+ if username != "" {
349+ return fmt .Errorf ("expected username to be empty or not present, got %q" , username )
350+ }
351+ }
352+ // If username doesn't exist, that's also valid (SDK behavior)
353+ return nil
354+ }))
355+
356+ resource .Test (t , resource.TestCase {
357+ ProtoV6ProviderFactories : testAccProtoV6ProviderFactories ,
358+ Steps : []resource.TestStep {
359+ // Create item using resource
360+ {
361+ Config : tfconfig .CreateConfigBuilder ()(
362+ tfconfig .ProviderConfig (),
363+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
364+ ),
365+ },
366+ // Read item with data source
367+ {
368+ Config : tfconfig .CreateConfigBuilder ()(
369+ tfconfig .ProviderConfig (),
370+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
371+ tfconfig .ItemDataSourceConfig (map [string ]string {
372+ "vault" : testVaultID ,
373+ "title" : fmt .Sprintf ("%v" , initialAttrs ["title" ]),
374+ }),
375+ ),
376+ Check : resource .ComposeAggregateTestCheckFunc (initialReadChecks ... ),
377+ },
378+ // Manually update item
379+ {
380+ Config : tfconfig .CreateConfigBuilder ()(
381+ tfconfig .ProviderConfig (),
382+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
383+ tfconfig .ItemDataSourceConfig (map [string ]string {
384+ "vault" : testVaultID ,
385+ "title" : fmt .Sprintf ("%v" , initialAttrs ["title" ]),
386+ }),
387+ ),
388+ Check : updateItemOutsideTerraform (),
389+ ExpectNonEmptyPlan : true ,
390+ },
391+ // Data source should read the updated values
392+ {
393+ Config : tfconfig .CreateConfigBuilder ()(
394+ tfconfig .ProviderConfig (),
395+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
396+ tfconfig .ItemDataSourceConfig (map [string ]string {
397+ "vault" : testVaultID ,
398+ "title" : fmt .Sprintf ("%v" , initialAttrs ["title" ]),
399+ }),
400+ ),
401+ Check : resource .ComposeAggregateTestCheckFunc (updatedReadChecks ... ),
402+ },
403+ // Manually remove fields
404+ {
405+ Config : tfconfig .CreateConfigBuilder ()(
406+ tfconfig .ProviderConfig (),
407+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
408+ tfconfig .ItemDataSourceConfig (map [string ]string {
409+ "vault" : testVaultID ,
410+ "title" : fmt .Sprintf ("%v" , initialAttrs ["title" ]),
411+ }),
412+ ),
413+ Check : removeFieldsOutsideTerraform (),
414+ ExpectNonEmptyPlan : true ,
415+ },
416+ // Data source should read the removed fields
417+ {
418+ Config : tfconfig .CreateConfigBuilder ()(
419+ tfconfig .ProviderConfig (),
420+ tfconfig .ItemResourceConfig (testVaultID , initialAttrs ),
421+ tfconfig .ItemDataSourceConfig (map [string ]string {
422+ "vault" : testVaultID ,
423+ "title" : fmt .Sprintf ("%v" , initialAttrs ["title" ]),
424+ }),
425+ ),
426+ Check : resource .ComposeAggregateTestCheckFunc (removedFieldsReadChecks ... ),
427+ },
428+ },
429+ })
430+ }
0 commit comments