557629d2bf
gates / gates (push) Successful in 2m3s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
79 lines
2.5 KiB
Go
79 lines
2.5 KiB
Go
package web
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func postCreate(s *Server, id string) *httptest.ResponseRecorder {
|
|
form := url.Values{"customer_id": {id}, "customer_name": {"Tester"}, "email": {"t@felhom.example"}}
|
|
req := httptest.NewRequest(http.MethodPost, "/configs/new", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rr := httptest.NewRecorder()
|
|
s.handleConfigCreate(rr, req)
|
|
return rr
|
|
}
|
|
|
|
// R-728: a second submit for the same customer ID while the first is still between its duplicate check
|
|
// and its save must NOT create (and mint) a second time. The first create is held at the save by the
|
|
// test seam while the second runs to completion.
|
|
func TestConfigCreate_ConcurrentSubmitCreatesOnce(t *testing.T) {
|
|
s, _ := newTestServer(t)
|
|
var logBuf syncBuf
|
|
s.logger = log.New(&logBuf, "", 0)
|
|
|
|
started, release := make(chan struct{}), make(chan struct{})
|
|
var once sync.Once
|
|
s.beforeCreateSave = func(string) {
|
|
first := false
|
|
once.Do(func() { first = true })
|
|
if first {
|
|
close(started)
|
|
<-release
|
|
}
|
|
}
|
|
|
|
done := make(chan *httptest.ResponseRecorder)
|
|
go func() { done <- postCreate(s, "tester-2") }()
|
|
select {
|
|
case <-started:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the first create never reached the save")
|
|
}
|
|
second := postCreate(s, "tester-2")
|
|
close(release)
|
|
first := <-done
|
|
|
|
if first.Code != http.StatusSeeOther {
|
|
t.Fatalf("first create = %d, want 303", first.Code)
|
|
}
|
|
if n := strings.Count(logBuf.String(), "Customer config created: tester-2"); n != 1 {
|
|
t.Fatalf("customer created %d times for one press, want exactly 1:\n%s", n, logBuf.String())
|
|
}
|
|
if !strings.Contains(second.Body.String(), "already being created") {
|
|
t.Errorf("the second submit must be told a create is in progress; got %d", second.Code)
|
|
}
|
|
|
|
// The guard is released: a later create of the SAME id is refused by the ordinary duplicate check,
|
|
// not stuck behind the in-flight guard.
|
|
s.beforeCreateSave = nil
|
|
if body := postCreate(s, "tester-2").Body.String(); !strings.Contains(body, "already exists") {
|
|
t.Error("after the first create returned, a re-submit must meet the ordinary 'already exists' check")
|
|
}
|
|
}
|
|
|
|
// syncBuf is a goroutine-safe log sink.
|
|
type syncBuf struct {
|
|
mu sync.Mutex
|
|
b strings.Builder
|
|
}
|
|
|
|
func (w *syncBuf) Write(p []byte) (int, error) { w.mu.Lock(); defer w.mu.Unlock(); return w.b.Write(p) }
|
|
func (w *syncBuf) String() string { w.mu.Lock(); defer w.mu.Unlock(); return w.b.String() }
|