terraform/internal/backend/remote-state/http/test_backend.go
Sarah French eab282f617
test: Add http backend locking test (#38192)
* test: Update TestHTTPBackend so it can be created with pre-existing lock info inside it. This will be used for tests that want to assert the lock info returned with lock errors.

* test: Add a test asserting data returned in lock errors from the http backend.

* chore: Replace use of `io/ioutil` in http backend

* chore: Update changelog entry to link to the original PR https://github.com/hashicorp/terraform/pull/38144

* test: Improve test code comments
2026-02-18 18:51:43 +00:00

150 lines
3.2 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package http
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"github.com/hashicorp/terraform/internal/states/statemgr"
)
type TestRequestHandleFunc func(w http.ResponseWriter, r *http.Request)
type TestHTTPBackend struct {
Data []byte
Locked bool
// LockInfo is set by the calling test code and is not
// set when tests use the Lock method on an http backend.
LockInfo *statemgr.LockInfo
methodFuncs map[string]TestRequestHandleFunc
methodCalls map[string]int
}
func (h *TestHTTPBackend) Handle(w http.ResponseWriter, r *http.Request) {
h.countMethodCall(r.Method)
called := h.callMethod(r.Method, w, r)
if called {
return
}
switch r.Method {
case "GET":
w.Write(h.Data)
case "PUT":
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r.Body); err != nil {
w.WriteHeader(500)
}
w.WriteHeader(201)
h.Data = buf.Bytes()
case "POST":
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r.Body); err != nil {
w.WriteHeader(500)
}
h.Data = buf.Bytes()
case "LOCK":
if h.Locked {
w.WriteHeader(423)
if h.LockInfo != nil {
// Write lock info to response, but only if
// the test http backend server with lock info present.
jsonLockInfo, err := json.Marshal(h.LockInfo)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("Failed to marshal lock info in test http backend server: " + err.Error()))
return
}
w.Write(jsonLockInfo)
}
} else {
h.Locked = true
}
case "UNLOCK":
h.Locked = false
case "DELETE":
h.Data = nil
w.WriteHeader(200)
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
}
}
func (h *TestHTTPBackend) countMethodCall(method string) {
if h.methodCalls == nil {
h.methodCalls = make(map[string]int)
}
if _, ok := h.methodCalls[method]; !ok {
h.methodCalls[method] = 0
}
h.methodCalls[method]++
}
func (h *TestHTTPBackend) CallCount(method string) int {
if h.methodCalls == nil {
return 0
}
callCount, ok := h.methodCalls[method]
if !ok {
return 0
}
return callCount
}
func (h *TestHTTPBackend) callMethod(method string, w http.ResponseWriter, r *http.Request) bool {
if h.methodFuncs == nil {
return false
}
f, ok := h.methodFuncs[method]
if ok {
f(w, r)
}
return ok
}
func (h *TestHTTPBackend) SetMethodFunc(method string, impl TestRequestHandleFunc) {
if h.methodFuncs == nil {
h.methodFuncs = make(map[string]TestRequestHandleFunc)
}
h.methodFuncs[method] = impl
}
// mod_dav-ish behavior
func (h *TestHTTPBackend) HandleWebDAV(w http.ResponseWriter, r *http.Request) {
h.countMethodCall(r.Method)
if f, ok := h.methodFuncs[r.Method]; ok {
f(w, r)
return
}
switch r.Method {
case "GET":
w.Write(h.Data)
case "PUT":
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r.Body); err != nil {
w.WriteHeader(500)
}
if reflect.DeepEqual(h.Data, buf.Bytes()) {
h.Data = buf.Bytes()
w.WriteHeader(204)
} else {
h.Data = buf.Bytes()
w.WriteHeader(201)
}
case "DELETE":
h.Data = nil
w.WriteHeader(200)
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
}
}