"force-unlock" support for the HTTP backend (#2381)

Signed-off-by: Bari, Haider <haider.bari@fmr.com>
Co-authored-by: Bari, Haider <haider.bari@fmr.com>
This commit is contained in:
Haider Bari 2025-01-23 00:05:57 +00:00 committed by GitHub
parent ebc900bec9
commit be72380cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 143 additions and 1 deletions

View File

@ -144,7 +144,25 @@ func (c *httpClient) Unlock(id string) error {
return nil
}
resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, c.jsonLockInfo, "unlock")
var lockInfo statemgr.LockInfo
// force unlock command does not instantiate statemgr.LockInfo
// which means that c.jsonLockInfo will be nil
if c.jsonLockInfo != nil {
if err := json.Unmarshal(c.jsonLockInfo, &lockInfo); err != nil { //nolint:musttag // for now add musttag until we fully adopt the linting rules
return fmt.Errorf("failed to unmarshal jsonLockInfo: %w", err)
}
if lockInfo.ID != id {
return &statemgr.LockError{
Info: &lockInfo,
Err: fmt.Errorf("lock id %q does not match existing lock", id),
}
}
}
lockInfo.ID = id
resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, lockInfo.Marshal(), "unlock")
if err != nil {
return err
}

View File

@ -254,6 +254,130 @@ func TestHttpClient_IsLockingEnabled(t *testing.T) {
}
}
// Tests the UnLock method for the HTTP client.
func TestHttpClient_Unlock(t *testing.T) {
stateLockInfoA := statemgr.LockInfo{
ID: "bjarne-stroustrup-state-lock-id",
Who: "BjarneStroustrup",
Operation: "TestTypePlan",
Created: time.Date(2023, time.August, 21, 15, 9, 26, 0, time.UTC),
}
stateLockInfoB := statemgr.LockInfo{
ID: "edsger-dijkstra-state-lock-id",
}
testCases := []struct {
name string
lockID string
jsonLockInfo []byte
lockResponseStatus int
lockResponseBody []byte
expectedErrorMsg error
expectedPayload []byte
}{
{
// Successful unlocking HTTP remote state
name: "Successfully unlocked",
lockID: stateLockInfoA.ID,
jsonLockInfo: stateLockInfoA.Marshal(),
lockResponseStatus: http.StatusOK,
lockResponseBody: nil,
expectedErrorMsg: nil,
expectedPayload: stateLockInfoA.Marshal(),
},
{
// Lock ID parameter does not match with LockInfo object Lock ID
name: "Lock ID's don't match",
lockID: stateLockInfoB.ID,
jsonLockInfo: stateLockInfoA.Marshal(),
lockResponseStatus: 0,
lockResponseBody: nil,
expectedErrorMsg: &statemgr.LockError{
Info: &stateLockInfoA,
Err: fmt.Errorf("lock id %q does not match existing lock", stateLockInfoB.ID),
},
expectedPayload: nil,
},
{
// Failed unmarshal jsonLockInfo into LockInfo object
name: "Failed to unmarshal jsonLockInfo",
lockID: stateLockInfoA.ID,
jsonLockInfo: []byte("Simplicity is prerequisite for reliability."),
lockResponseStatus: 0,
lockResponseBody: nil,
expectedErrorMsg: fmt.Errorf("failed to unmarshal jsonLockInfo: invalid character 'S' looking for beginning of value"),
expectedPayload: nil,
},
{
// Force unlock command being executed
name: "Successful force unlock",
lockID: stateLockInfoB.ID,
jsonLockInfo: nil,
lockResponseStatus: http.StatusOK,
lockResponseBody: nil,
expectedErrorMsg: nil,
expectedPayload: stateLockInfoB.Marshal(),
},
{
// Force unlock command being executed
name: "Unsuccessful force unlock",
lockID: stateLockInfoB.ID,
jsonLockInfo: nil,
lockResponseStatus: http.StatusNotFound,
lockResponseBody: nil,
expectedErrorMsg: fmt.Errorf("Unexpected HTTP response code %d", http.StatusNotFound),
expectedPayload: stateLockInfoB.Marshal(),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
var receivedPayload []byte
handler := func(w http.ResponseWriter, r *http.Request) {
receivedPayload, _ = io.ReadAll(r.Body)
w.WriteHeader(tt.lockResponseStatus)
_, err := w.Write(tt.lockResponseBody)
if err != nil {
t.Fatalf("Failed to write response body: %v", err)
}
}
ts := httptest.NewServer(http.HandlerFunc(handler))
defer ts.Close()
unlockURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("Failed to parse lockURL: %v", err)
}
client := &httpClient{
UnlockURL: unlockURL,
LockMethod: "UNLOCK",
Client: retryablehttp.NewClient(),
jsonLockInfo: tt.jsonLockInfo,
}
err = client.Unlock(tt.lockID)
if tt.expectedErrorMsg != nil && err == nil {
// no expected error
t.Errorf("UnLock() no expected error = %v", tt.expectedErrorMsg)
}
if tt.expectedErrorMsg == nil && err != nil {
// unexpected error
t.Errorf("UnLock() unexpected error = %v", err)
}
if tt.expectedErrorMsg != nil && err.Error() != tt.expectedErrorMsg.Error() {
// mismatched errors
t.Errorf("UnLock() error = %v, want %v", err, tt.expectedErrorMsg)
}
if !bytes.Equal(receivedPayload, tt.expectedPayload) {
t.Errorf("UnLock() payload = %v, want %v", receivedPayload, tt.expectedPayload)
}
})
}
}
// Tests the Lock method for the HTTP client.
// Test to see correct lock info is returned
func TestHttpClient_lock(t *testing.T) {