139 lines
6.3 KiB
Go
139 lines
6.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
|
|
)
|
|
|
|
func TestUploadTransportFailureIsSingleAttemptAndRedactsSignedURL(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "recording.bin")
|
|
if err := os.WriteFile(path, []byte("bytes"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var attempts int
|
|
client := &http.Client{Transport: uploadFailureTransport{calls: &attempts}}
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-error", ObjectKey: "recording", TargetUrl: "https://oss.example.invalid/object?signature=DO_NOT_LOG", MaxBytes: 5, ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli()}
|
|
_, err := (UploadClient{HTTPClient: client}).UploadFile(context.Background(), grant, path)
|
|
if err == nil {
|
|
t.Fatal("transport failure hidden")
|
|
}
|
|
if strings.Contains(err.Error(), "DO_NOT_LOG") || strings.Contains(err.Error(), "signature=") {
|
|
t.Fatal("signed URL leaked in error")
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("upload attempted %d times", attempts)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatal("failed upload lost its source file")
|
|
}
|
|
}
|
|
|
|
type uploadFailureTransport struct{ calls *int }
|
|
|
|
func (t uploadFailureTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
|
|
*t.calls++
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
|
|
func TestUploadClientUsesGrantAndVerifiesChecksum(t *testing.T) {
|
|
body := []byte("mock recording bytes")
|
|
digest := sha256.Sum256(body)
|
|
var received []byte
|
|
var receivedHeader string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
receivedHeader = r.Header.Get("x-upload-token")
|
|
received, _ = io.ReadAll(r.Body)
|
|
w.Header().Set("ETag", "etag-1")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
assetPath := filepath.Join(t.TempDir(), "recording.bin")
|
|
if err := os.WriteFile(assetPath, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-1", TargetUrl: server.URL, ObjectKey: "recording-1", ExpiresAtUnixMs: time.Unix(101, 0).UnixMilli(), Headers: []*agentv1.Header{{Name: "x-upload-token", Value: "mock-token"}}, RequiredChecksumSha256: hex.EncodeToString(digest[:]), MaxBytes: int64(len(body))}
|
|
result, err := (UploadClient{AllowInsecureHTTP: true, Now: func() time.Time { return time.Unix(100, 0) }, AllowedHosts: map[string]struct{}{strings.TrimPrefix(server.URL, "http://"): {}}}).UploadFile(context.Background(), grant, assetPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.StatusCode != http.StatusOK || result.SHA256 != grant.RequiredChecksumSha256 || result.ETag != "etag-1" || string(received) != string(body) || receivedHeader != "mock-token" {
|
|
t.Fatalf("unexpected upload result: %+v body=%q header=%q", result, received, receivedHeader)
|
|
}
|
|
if retained, statErr := os.Stat(assetPath); statErr != nil || retained.Size() != int64(len(body)) {
|
|
t.Fatalf("source asset was not retained after upload: stat=%v info=%v", statErr, retained)
|
|
}
|
|
}
|
|
|
|
func TestUploadClientFailsClosedForGrantMismatchAndHTTP(t *testing.T) {
|
|
assetPath := filepath.Join(t.TempDir(), "recording.bin")
|
|
if err := os.WriteFile(assetPath, []byte("bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-2", TargetUrl: "http://127.0.0.1:1/upload", ObjectKey: "recording-2", ExpiresAtUnixMs: time.Now().Add(time.Hour).UnixMilli(), RequiredChecksumSha256: strings.Repeat("a", 64), MaxBytes: 1024}
|
|
if _, err := (UploadClient{AllowInsecureHTTP: true}).UploadFile(context.Background(), grant, assetPath); !errors.Is(err, ErrUploadChecksumMismatch) {
|
|
t.Fatalf("expected typed checksum mismatch, got %v", err)
|
|
}
|
|
grant.RequiredChecksumSha256 = ""
|
|
if _, err := (UploadClient{}).UploadFile(context.Background(), grant, assetPath); !errors.Is(err, ErrUploadGrantInvalid) {
|
|
t.Fatalf("expected typed grant rejection, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUploadClientHTTPFailureHasStatusWithoutResponseBody(t *testing.T) {
|
|
assetPath := filepath.Join(t.TempDir(), "recording.bin")
|
|
body := []byte("bytes")
|
|
if err := os.WriteFile(assetPath, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_, _ = w.Write([]byte("DO_NOT_LOG_SECRET"))
|
|
}))
|
|
defer server.Close()
|
|
sum := sha256.Sum256(body)
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-forbidden", TargetUrl: server.URL, ObjectKey: "recording-forbidden", ExpiresAtUnixMs: time.Now().Add(time.Minute).UnixMilli(), RequiredChecksumSha256: hex.EncodeToString(sum[:]), MaxBytes: 1024}
|
|
_, err := (UploadClient{AllowInsecureHTTP: true}).UploadFile(context.Background(), grant, assetPath)
|
|
var response *UploadHTTPError
|
|
if !errors.As(err, &response) || response.StatusCode != http.StatusForbidden || strings.Contains(err.Error(), "DO_NOT_LOG_SECRET") {
|
|
t.Fatalf("explicit PUT rejection was not typed and redacted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUploadClientEnforcesSizeAndHost(t *testing.T) {
|
|
assetPath := filepath.Join(t.TempDir(), "recording.bin")
|
|
if err := os.WriteFile(assetPath, []byte("bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-3", TargetUrl: "https://oss.example.invalid/upload", ObjectKey: "recording-3", ExpiresAtUnixMs: time.Now().Add(time.Hour).UnixMilli(), MaxBytes: 1}
|
|
if _, err := (UploadClient{}).UploadFile(context.Background(), grant, assetPath); err == nil {
|
|
t.Fatal("expected size rejection")
|
|
}
|
|
grant.MaxBytes = 1024
|
|
if _, err := (UploadClient{AllowedHosts: map[string]struct{}{"other.example.invalid": {}}}).UploadFile(context.Background(), grant, assetPath); err == nil {
|
|
t.Fatal("expected host rejection")
|
|
}
|
|
}
|
|
|
|
func TestUploadClientRejectsExpiredGrant(t *testing.T) {
|
|
assetPath := filepath.Join(t.TempDir(), "recording.bin")
|
|
if err := os.WriteFile(assetPath, []byte("bytes"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grant := &agentv1.UploadGrant{UploadId: "upload-expired", TargetUrl: "https://oss.example.invalid/upload", ObjectKey: "recording-expired", ExpiresAtUnixMs: time.Unix(100, 0).UnixMilli(), MaxBytes: 1024}
|
|
if _, err := (UploadClient{Now: func() time.Time { return time.Unix(100, 0) }}).UploadFile(context.Background(), grant, assetPath); !errors.Is(err, ErrUploadGrantExpired) {
|
|
t.Fatalf("expected typed expired grant rejection, got %v", err)
|
|
}
|
|
}
|