package agent import ( "context" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" ) 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 := &agentpb.UploadGrant{UploadId: "upload-error", Bucket: "mock-bucket", 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 := &agentpb.UploadGrant{UploadId: "upload-1", Bucket: "mock-bucket", TargetUrl: server.URL, ObjectKey: "recording-1", ExpiresAtUnixMs: time.Unix(101, 0).UnixMilli(), Headers: []*agentpb.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 := &agentpb.UploadGrant{UploadId: "upload-2", Bucket: "mock-bucket", 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 := &agentpb.UploadGrant{UploadId: "upload-forbidden", Bucket: "mock-bucket", 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 := &agentpb.UploadGrant{UploadId: "upload-3", Bucket: "mock-bucket", 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 := &agentpb.UploadGrant{UploadId: "upload-expired", Bucket: "mock-bucket", 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) } }