11package coderefs
22
33import (
4+ "context"
45 "os"
56 "testing"
67
8+ "github.com/stretchr/testify/require"
9+
10+ "github.com/bucketeer-io/code-refs/internal/bucketeer"
711 "github.com/bucketeer-io/code-refs/internal/log"
12+ "github.com/bucketeer-io/code-refs/options"
813)
914
1015func init () {
@@ -14,6 +19,117 @@ func TestMain(m *testing.M) {
1419 log .Init (true )
1520 os .Exit (m .Run ())
1621}
22+
23+ type fakeApiClient struct {
24+ existing []bucketeer.CodeReference
25+ created []bucketeer.CodeReference
26+ updated map [string ]bucketeer.CodeReference
27+ deleted []string
28+ }
29+
30+ func (f * fakeApiClient ) GetFlagKeyList (_ context.Context , _ options.Options ) ([]string , error ) {
31+ return nil , nil
32+ }
33+
34+ func (f * fakeApiClient ) CreateCodeReference (_ context.Context , _ options.Options , ref bucketeer.CodeReference ) error {
35+ f .created = append (f .created , ref )
36+ return nil
37+ }
38+
39+ func (f * fakeApiClient ) UpdateCodeReference (_ context.Context , _ options.Options , id string , ref bucketeer.CodeReference ) error {
40+ f .updated [id ] = ref
41+ return nil
42+ }
43+
44+ func (f * fakeApiClient ) DeleteCodeReference (_ context.Context , _ options.Options , id string ) error {
45+ f .deleted = append (f .deleted , id )
46+ return nil
47+ }
48+
49+ func (f * fakeApiClient ) ListCodeReferences (_ context.Context , _ options.Options , _ string , _ int64 ) ([]bucketeer.CodeReference , string , string , error ) {
50+ return f .existing , "" , "" , nil
51+ }
52+
53+ func Test_processCodeReferences_identicalContentHashes (t * testing.T ) {
54+ // two refs for the same flag whose snippets are textually identical (e.g.
55+ // different secrets both redacted to the same text) share a ContentHash;
56+ // each must still reconcile with its own scanned hunk instead of one
57+ // overwriting the other and the second being recreated
58+ api := & fakeApiClient {
59+ existing : []bucketeer.CodeReference {
60+ {ID : "ref-a" , FeatureID : "my-flag" , FilePath : "a.go" , ContentHash : "same-hash" , RepositoryOwner : "owner" , RepositoryName : "repo" },
61+ {ID : "ref-b" , FeatureID : "my-flag" , FilePath : "b.go" , ContentHash : "same-hash" , RepositoryOwner : "owner" , RepositoryName : "repo" },
62+ },
63+ updated : map [string ]bucketeer.CodeReference {},
64+ }
65+ opts := options.Options {RepoOwner : "owner" , RepoName : "repo" }
66+ refs := []bucketeer.ReferenceHunksRep {
67+ {Path : "a.go" , Hunks : []bucketeer.HunkRep {{FlagKey : "my-flag" , ContentHash : "same-hash" , Lines : `key = "[REDACTED]"` }}},
68+ {Path : "b.go" , Hunks : []bucketeer.HunkRep {{FlagKey : "my-flag" , ContentHash : "same-hash" , Lines : `key = "[REDACTED]"` }}},
69+ }
70+
71+ processCodeReferences (opts , api , refs , "main" , "abc123" , "GITHUB" )
72+
73+ require .Empty (t , api .created )
74+ require .Empty (t , api .deleted )
75+ require .Len (t , api .updated , 2 )
76+ require .Equal (t , "a.go" , api .updated ["ref-a" ].FilePath )
77+ require .Equal (t , "b.go" , api .updated ["ref-b" ].FilePath )
78+ }
79+
80+ func Test_processCodeReferences_duplicateReferenceKeys (t * testing.T ) {
81+ // references that share even the composite key (identical snippets of one
82+ // flag in one file, or every hunk when contextLines < 0 hashes the empty
83+ // string) must reconcile one-to-one instead of the duplicate row being
84+ // dropped from the map and accumulating on the server run after run
85+ api := & fakeApiClient {
86+ existing : []bucketeer.CodeReference {
87+ {ID : "ref-a" , FeatureID : "my-flag" , FilePath : "a.go" , ContentHash : "h" , RepositoryOwner : "owner" , RepositoryName : "repo" },
88+ {ID : "ref-b" , FeatureID : "my-flag" , FilePath : "a.go" , ContentHash : "h" , RepositoryOwner : "owner" , RepositoryName : "repo" },
89+ },
90+ updated : map [string ]bucketeer.CodeReference {},
91+ }
92+ opts := options.Options {RepoOwner : "owner" , RepoName : "repo" }
93+ refs := []bucketeer.ReferenceHunksRep {
94+ {Path : "a.go" , Hunks : []bucketeer.HunkRep {
95+ {FlagKey : "my-flag" , ContentHash : "h" },
96+ {FlagKey : "my-flag" , ContentHash : "h" },
97+ }},
98+ }
99+
100+ processCodeReferences (opts , api , refs , "main" , "abc123" , "GITHUB" )
101+
102+ require .Empty (t , api .created )
103+ require .Empty (t , api .deleted )
104+ require .Len (t , api .updated , 2 )
105+ require .Contains (t , api .updated , "ref-a" )
106+ require .Contains (t , api .updated , "ref-b" )
107+ }
108+
109+ func Test_processCodeReferences_backfillsMissingFeatureID (t * testing.T ) {
110+ // the list endpoint is queried by featureId; if the server omits the
111+ // field in response items, the queried flag must be backfilled so the
112+ // reconciliation key still matches instead of every ref being recreated
113+ // and the old rows deleted
114+ api := & fakeApiClient {
115+ existing : []bucketeer.CodeReference {
116+ {ID : "ref-a" , FilePath : "a.go" , ContentHash : "h" , RepositoryOwner : "owner" , RepositoryName : "repo" },
117+ },
118+ updated : map [string ]bucketeer.CodeReference {},
119+ }
120+ opts := options.Options {RepoOwner : "owner" , RepoName : "repo" }
121+ refs := []bucketeer.ReferenceHunksRep {
122+ {Path : "a.go" , Hunks : []bucketeer.HunkRep {{FlagKey : "my-flag" , ContentHash : "h" }}},
123+ }
124+
125+ processCodeReferences (opts , api , refs , "main" , "abc123" , "GITHUB" )
126+
127+ require .Empty (t , api .created )
128+ require .Empty (t , api .deleted )
129+ require .Len (t , api .updated , 1 )
130+ require .Contains (t , api .updated , "ref-a" )
131+ }
132+
17133// func Test_calculateStaleBranches(t *testing.T) {
18134// specs := []struct {
19135// name string
0 commit comments