102 lines
5.1 KiB
Go
102 lines
5.1 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
)
|
|
|
|
func TestUploadBytesPutsMemoryRecordingOnceWithoutBusinessFile(t *testing.T) {
|
|
body := []byte("recording held only in memory")
|
|
sum := sha256.Sum256(body)
|
|
var requests atomic.Int32
|
|
var received []byte
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
if r.Method != http.MethodPut || r.ContentLength != int64(len(body)) || r.Header.Get("x-upload-token") != "mock-token" {
|
|
http.Error(w, "invalid presigned PUT", http.StatusBadRequest)
|
|
return
|
|
}
|
|
received, _ = io.ReadAll(r.Body)
|
|
w.Header().Set("ETag", "mock-etag")
|
|
}))
|
|
defer server.Close()
|
|
noFiles := t.TempDir()
|
|
t.Setenv("TMPDIR", noFiles)
|
|
grant := &agentpb.UploadGrant{UploadId: "upload-mem", ObjectKey: "recordings/call-1.wav", TargetUrl: server.URL + "/file?signature=DO_NOT_LOG", ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), MaxBytes: int64(len(body)), RequiredChecksumSha256: hex.EncodeToString(sum[:]), Headers: []*agentpb.Header{{Name: "x-upload-token", Value: "mock-token"}}}
|
|
result, err := (UploadClient{AllowInsecureHTTP: true}).UploadBytes(context.Background(), grant, body)
|
|
if err != nil || requests.Load() != 1 || !bytes.Equal(received, body) || result.SHA256 != grant.RequiredChecksumSha256 || result.SizeBytes != int64(len(body)) || result.ETag != "mock-etag" {
|
|
t.Fatalf("normal path must PUT original bytes once: result=%+v requests=%d err=%v", result, requests.Load(), err)
|
|
}
|
|
entries, err := os.ReadDir(noFiles)
|
|
if err != nil || len(entries) != 0 {
|
|
t.Fatalf("normal path created a business file: entries=%v err=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestUploadBytesRejectsUnchangedGrantMismatchBeforePUT(t *testing.T) {
|
|
var requests atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { requests.Add(1) }))
|
|
defer server.Close()
|
|
data := []byte("original recording")
|
|
grant := &agentpb.UploadGrant{UploadId: "upload-mismatch", ObjectKey: "recordings/call-2.wav", TargetUrl: server.URL, ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), MaxBytes: int64(len(data)), RequiredChecksumSha256: strings.Repeat("f", 64)}
|
|
_, err := (UploadClient{AllowInsecureHTTP: true}).UploadBytes(context.Background(), grant, data)
|
|
if !errors.Is(err, ErrUploadChecksumMismatch) || requests.Load() != 0 || string(data) != "original recording" {
|
|
t.Fatalf("mismatched grant caused a PUT or changed recording: requests=%d err=%v", requests.Load(), err)
|
|
}
|
|
}
|
|
|
|
func TestUploadBytesDefinitiveHTTPFailureIsOneAttempt(t *testing.T) {
|
|
var requests atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests.Add(1)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
_, _ = io.WriteString(w, "DO_NOT_LOG_RESPONSE")
|
|
}))
|
|
defer server.Close()
|
|
grant := &agentpb.UploadGrant{UploadId: "upload-fail", ObjectKey: "recordings/call-3.wav", TargetUrl: server.URL, ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), MaxBytes: 100}
|
|
_, err := (UploadClient{AllowInsecureHTTP: true}).UploadBytes(context.Background(), grant, []byte("recording"))
|
|
var rejected *UploadHTTPError
|
|
if !errors.As(err, &rejected) || rejected.StatusCode != http.StatusServiceUnavailable || requests.Load() != 1 || strings.Contains(err.Error(), "DO_NOT_LOG_RESPONSE") {
|
|
t.Fatalf("failed OSS PUT was retried or leaked its response: requests=%d err=%v", requests.Load(), err)
|
|
}
|
|
}
|
|
|
|
type ambiguousBytesTransport struct{ requests *atomic.Int32 }
|
|
|
|
func (t ambiguousBytesTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
t.requests.Add(1)
|
|
return nil, fmt.Errorf("transport says URL=%s", req.URL.String())
|
|
}
|
|
|
|
func TestUploadBytesAmbiguousTransportFailureIsNotRetriedOrLeaked(t *testing.T) {
|
|
var requests atomic.Int32
|
|
client := &http.Client{Transport: ambiguousBytesTransport{requests: &requests}}
|
|
grant := &agentpb.UploadGrant{UploadId: "upload-unknown", ObjectKey: "recordings/call-4.wav", TargetUrl: "https://oss.example.invalid/file?signature=DO_NOT_LOG", ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), MaxBytes: 100}
|
|
_, err := (UploadClient{HTTPClient: client}).UploadBytes(context.Background(), grant, []byte("recording"))
|
|
if !errors.Is(err, ErrUploadOutcomeUnknown) || requests.Load() != 1 || strings.Contains(err.Error(), "DO_NOT_LOG") || strings.Contains(err.Error(), "signature=") {
|
|
t.Fatalf("ambiguous upload must halt without token disclosure: requests=%d err=%v", requests.Load(), err)
|
|
}
|
|
}
|
|
|
|
func TestUploadBytesRejectsNilGrantHeaderBeforePUT(t *testing.T) {
|
|
grant := &agentpb.UploadGrant{UploadId: "upload-bad-header", ObjectKey: "recordings/call-5.wav", TargetUrl: "https://oss.example.invalid/file", ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), MaxBytes: 100, Headers: []*agentpb.Header{nil}}
|
|
_, err := (UploadClient{}).UploadBytes(context.Background(), grant, []byte("recording"))
|
|
if !errors.Is(err, ErrUploadGrantInvalid) {
|
|
t.Fatalf("malformed grant header must be rejected without a panic: %v", err)
|
|
}
|
|
}
|