@@ -1122,81 +1122,179 @@ func Test_CreateOrUpdateFile(t *testing.T) {
11221122 expectedErrMsg : "failed to create/update file" ,
11231123 },
11241124 {
1125- name : "file creation fails with missing sha, then succeeds after fetching sha " ,
1125+ name : "sha validation - current sha matches (304 Not Modified) " ,
11261126 mockedClient : mock .NewMockedHTTPClient (
11271127 mock .WithRequestMatchHandler (
1128- mock .PutReposContentsByOwnerByRepoByPath ,
1129- func () http.HandlerFunc {
1130- callCount := 0
1131- return func (w http.ResponseWriter , _ * http.Request ) {
1132- callCount ++
1133- if callCount == 1 {
1134- // First call fails with "sha wasn't supplied" error
1135- w .WriteHeader (http .StatusUnprocessableEntity )
1136- _ , _ = w .Write ([]byte (`{"message": "\"sha\" wasn't supplied"}` ))
1137- } else {
1138- // Second call succeeds after SHA is retrieved
1139- w .WriteHeader (http .StatusOK )
1140- respBytes , _ := json .Marshal (mockFileResponse )
1141- _ , _ = w .Write (respBytes )
1142- }
1128+ mock.EndpointPattern {
1129+ Pattern : "/repos/owner/repo/contents/docs/example.md" ,
1130+ Method : "HEAD" ,
1131+ },
1132+ http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
1133+ // Verify If-None-Match header is set correctly
1134+ ifNoneMatch := req .Header .Get ("If-None-Match" )
1135+ if ifNoneMatch == `"abc123def456"` {
1136+ w .WriteHeader (http .StatusNotModified )
1137+ } else {
1138+ w .WriteHeader (http .StatusOK )
1139+ w .Header ().Set ("ETag" , `"abc123def456"` )
11431140 }
1144- }( ),
1141+ }),
11451142 ),
11461143 mock .WithRequestMatchHandler (
1147- mock .GetReposContentsByOwnerByRepoByPath ,
1144+ mock .PutReposContentsByOwnerByRepoByPath ,
1145+ expectRequestBody (t , map [string ]interface {}{
1146+ "message" : "Update example file" ,
1147+ "content" : "IyBVcGRhdGVkIEV4YW1wbGUKClRoaXMgZmlsZSBoYXMgYmVlbiB1cGRhdGVkLg==" ,
1148+ "branch" : "main" ,
1149+ "sha" : "abc123def456" ,
1150+ }).andThen (
1151+ mockResponse (t , http .StatusOK , mockFileResponse ),
1152+ ),
1153+ ),
1154+ ),
1155+ requestArgs : map [string ]interface {}{
1156+ "owner" : "owner" ,
1157+ "repo" : "repo" ,
1158+ "path" : "docs/example.md" ,
1159+ "content" : "# Updated Example\n \n This file has been updated." ,
1160+ "message" : "Update example file" ,
1161+ "branch" : "main" ,
1162+ "sha" : "abc123def456" ,
1163+ },
1164+ expectError : false ,
1165+ expectedContent : mockFileResponse ,
1166+ },
1167+ {
1168+ name : "sha validation - stale sha detected (200 OK with different ETag)" ,
1169+ mockedClient : mock .NewMockedHTTPClient (
1170+ mock .WithRequestMatchHandler (
1171+ mock.EndpointPattern {
1172+ Pattern : "/repos/owner/repo/contents/docs/example.md" ,
1173+ Method : "HEAD" ,
1174+ },
11481175 http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
1176+ // SHA doesn't match - return 200 with current ETag
1177+ w .Header ().Set ("ETag" , `"newsha999888"` )
11491178 w .WriteHeader (http .StatusOK )
1150- existingFile := & github.RepositoryContent {
1151- Name : github .Ptr ("example.md" ),
1152- Path : github .Ptr ("docs/example.md" ),
1153- SHA : github .Ptr ("abc123def456" ),
1154- Type : github .Ptr ("file" ),
1155- }
1156- contentBytes , _ := json .Marshal (existingFile )
1157- _ , _ = w .Write (contentBytes )
11581179 }),
11591180 ),
11601181 ),
11611182 requestArgs : map [string ]interface {}{
11621183 "owner" : "owner" ,
11631184 "repo" : "repo" ,
11641185 "path" : "docs/example.md" ,
1165- "content" : "# Example\n \n This is an example file." ,
1166- "message" : "Add example file" ,
1186+ "content" : "# Updated Example\n \n This file has been updated." ,
1187+ "message" : "Update example file" ,
1188+ "branch" : "main" ,
1189+ "sha" : "oldsha123456" ,
1190+ },
1191+ expectError : true ,
1192+ expectedErrMsg : "SHA mismatch: provided SHA oldsha123456 is stale. Current file SHA is newsha999888" ,
1193+ },
1194+ {
1195+ name : "sha validation - file doesn't exist (404), proceed with create" ,
1196+ mockedClient : mock .NewMockedHTTPClient (
1197+ mock .WithRequestMatchHandler (
1198+ mock.EndpointPattern {
1199+ Pattern : "/repos/owner/repo/contents/docs/example.md" ,
1200+ Method : "HEAD" ,
1201+ },
1202+ http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
1203+ w .WriteHeader (http .StatusNotFound )
1204+ }),
1205+ ),
1206+ mock .WithRequestMatchHandler (
1207+ mock .PutReposContentsByOwnerByRepoByPath ,
1208+ expectRequestBody (t , map [string ]interface {}{
1209+ "message" : "Create new file" ,
1210+ "content" : "IyBOZXcgRmlsZQoKVGhpcyBpcyBhIG5ldyBmaWxlLg==" ,
1211+ "branch" : "main" ,
1212+ "sha" : "ignoredsha" , // SHA is sent but GitHub API ignores it for new files
1213+ }).andThen (
1214+ mockResponse (t , http .StatusCreated , mockFileResponse ),
1215+ ),
1216+ ),
1217+ ),
1218+ requestArgs : map [string ]interface {}{
1219+ "owner" : "owner" ,
1220+ "repo" : "repo" ,
1221+ "path" : "docs/example.md" ,
1222+ "content" : "# New File\n \n This is a new file." ,
1223+ "message" : "Create new file" ,
11671224 "branch" : "main" ,
1225+ "sha" : "ignoredsha" ,
11681226 },
11691227 expectError : false ,
11701228 expectedContent : mockFileResponse ,
11711229 },
11721230 {
1173- name : "file creation fails with missing sha and GetContents also fails " ,
1231+ name : "no sha provided - file exists, returns warning " ,
11741232 mockedClient : mock .NewMockedHTTPClient (
11751233 mock .WithRequestMatchHandler (
1176- mock .PutReposContentsByOwnerByRepoByPath ,
1234+ mock.EndpointPattern {
1235+ Pattern : "/repos/owner/repo/contents/docs/example.md" ,
1236+ Method : "HEAD" ,
1237+ },
11771238 http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
1178- w .WriteHeader ( http . StatusUnprocessableEntity )
1179- _ , _ = w . Write ([] byte ( `{"message": "\"sha\" wasn't supplied"}` ) )
1239+ w .Header (). Set ( "ETag" , `"existing123"` )
1240+ w . WriteHeader ( http . StatusOK )
11801241 }),
11811242 ),
11821243 mock .WithRequestMatchHandler (
1183- mock .GetReposContentsByOwnerByRepoByPath ,
1244+ mock .PutReposContentsByOwnerByRepoByPath ,
1245+ expectRequestBody (t , map [string ]interface {}{
1246+ "message" : "Update without SHA" ,
1247+ "content" : "IyBVcGRhdGVkCgpVcGRhdGVkIHdpdGhvdXQgU0hBLg==" ,
1248+ "branch" : "main" ,
1249+ }).andThen (
1250+ mockResponse (t , http .StatusOK , mockFileResponse ),
1251+ ),
1252+ ),
1253+ ),
1254+ requestArgs : map [string ]interface {}{
1255+ "owner" : "owner" ,
1256+ "repo" : "repo" ,
1257+ "path" : "docs/example.md" ,
1258+ "content" : "# Updated\n \n Updated without SHA." ,
1259+ "message" : "Update without SHA" ,
1260+ "branch" : "main" ,
1261+ },
1262+ expectError : false ,
1263+ expectedErrMsg : "Warning: File updated without SHA validation. Previous file SHA was existing123" ,
1264+ },
1265+ {
1266+ name : "no sha provided - file doesn't exist, no warning" ,
1267+ mockedClient : mock .NewMockedHTTPClient (
1268+ mock .WithRequestMatchHandler (
1269+ mock.EndpointPattern {
1270+ Pattern : "/repos/owner/repo/contents/docs/example.md" ,
1271+ Method : "HEAD" ,
1272+ },
11841273 http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
11851274 w .WriteHeader (http .StatusNotFound )
1186- _ , _ = w .Write ([]byte (`{"message": "Not Found"}` ))
11871275 }),
11881276 ),
1277+ mock .WithRequestMatchHandler (
1278+ mock .PutReposContentsByOwnerByRepoByPath ,
1279+ expectRequestBody (t , map [string ]interface {}{
1280+ "message" : "Create new file" ,
1281+ "content" : "IyBOZXcgRmlsZQoKQ3JlYXRlZCB3aXRob3V0IFNIQQ==" ,
1282+ "branch" : "main" ,
1283+ }).andThen (
1284+ mockResponse (t , http .StatusCreated , mockFileResponse ),
1285+ ),
1286+ ),
11891287 ),
11901288 requestArgs : map [string ]interface {}{
11911289 "owner" : "owner" ,
11921290 "repo" : "repo" ,
11931291 "path" : "docs/example.md" ,
1194- "content" : "# Example \n \n This is an example file. " ,
1195- "message" : "Add example file" ,
1292+ "content" : "# New File \n \n Created without SHA " ,
1293+ "message" : "Create new file" ,
11961294 "branch" : "main" ,
11971295 },
1198- expectError : true ,
1199- expectedErrMsg : "failed to get file SHA for update" ,
1296+ expectError : false ,
1297+ expectedContent : mockFileResponse ,
12001298 },
12011299 }
12021300
@@ -1227,6 +1325,12 @@ func Test_CreateOrUpdateFile(t *testing.T) {
12271325 // Parse the result and get the text content if no error
12281326 textContent := getTextResult (t , result )
12291327
1328+ // If expectedErrMsg is set (but expectError is false), this is a warning case
1329+ if tc .expectedErrMsg != "" {
1330+ assert .Contains (t , textContent .Text , tc .expectedErrMsg )
1331+ return
1332+ }
1333+
12301334 // Unmarshal and verify the result
12311335 var returnedContent github.RepositoryContentResponse
12321336 err = json .Unmarshal ([]byte (textContent .Text ), & returnedContent )
0 commit comments