remove distribution from hold, add vulnerability scanning in appview.

1. Removing distribution/distribution from the Hold Service (biggest change)
  The hold service previously used distribution's StorageDriver interface for all blob operations. This replaces it with direct AWS SDK v2 calls through ATCR's own pkg/s3.S3Service:
  - New S3Service methods: Stat(), PutBytes(), Move(), Delete(), WalkBlobs(), ListPrefix() added to pkg/s3/types.go
  - Pull zone fix: Presigned URLs are now generated against the real S3 endpoint, then the host is swapped to the CDN URL post-signing (previously the CDN URL was set as the endpoint, which
  broke SigV4 signatures)
  - All hold subsystems migrated: GC, OCI uploads, XRPC handlers, profile uploads, scan broadcaster, manifest posts — all now use *s3.S3Service instead of storagedriver.StorageDriver
  - Config simplified: Removed configuration.Storage type and buildStorageConfigFromFields(); replaced with a simple S3Params() method
  - Mock expanded: MockS3Client gains an in-memory object store + 5 new methods, replacing duplicate mockStorageDriver implementations in tests (~160 lines deleted from each test file)
2. Vulnerability Scan UI in AppView (new feature)
  Displays scan results from the hold's PDS on the repository page:
  - New lexicon: io/atcr/hold/scan.json with vulnReportBlob field for storing full Grype reports
  - Two new HTMX endpoints: /api/scan-result (badge) and /api/vuln-details (modal with CVE table)
  - New templates: vuln-badge.html (severity count chips) and vuln-details.html (full CVE table with NVD/GHSA links)
  - Repository page: Lazy-loads scan badges per manifest via HTMX
  - Tests: ~590 lines of test coverage for both handlers
3. S3 Diagnostic Tool
  New cmd/s3-test/main.go (418 lines) — tests S3 connectivity with both SDK v1 and v2, including presigned URL generation, pull zone host swapping, and verbose signing debug output.
4. Deployment Tooling
  - New syncServiceUnit() for comparing/updating systemd units on servers
  - Update command now syncs config keys (adds missing keys from template) and service units with daemon-reload
5. DB Migration
  0011_fix_captain_successor_column.yaml — rebuilds hold_captain_records to add the successor column that was missed in a previous migration.
6. Documentation
  - APPVIEW-UI-FUTURE.md rewritten as a status-tracked feature inventory
  - DISTRIBUTION.md renamed to CREDENTIAL_HELPER.md
  - New REMOVING_DISTRIBUTION.md — 480-line analysis of fully removing distribution from the appview side
7. go.mod
  aws-sdk-go v1 moved from indirect to direct (needed by cmd/s3-test).
This commit is contained in:
Evan Jarrett
2026-02-13 15:26:24 -06:00
parent 434a5f1eee
commit de02e1f046
38 changed files with 3134 additions and 962 deletions
+3 -4
View File
@@ -256,7 +256,7 @@ func (h *XRPCHandler) CompleteMultipartUploadWithManager(ctx context.Context, up
"source", sourcePath,
"dest", destPath)
if _, err := h.driver.Stat(ctx, sourcePath); err != nil {
if _, err := h.s3Service.Stat(ctx, sourcePath); err != nil {
slog.Error("Source blob not found after multipart complete",
"path", sourcePath,
"error", err)
@@ -264,9 +264,8 @@ func (h *XRPCHandler) CompleteMultipartUploadWithManager(ctx context.Context, up
}
slog.Debug("Source blob verified", "path", sourcePath)
// Move from temp to final digest location using driver
// Driver handles path management correctly (including S3 prefix)
if err := h.driver.Move(ctx, sourcePath, destPath); err != nil {
// Move from temp to final digest location (S3 copy + delete)
if err := h.s3Service.Move(ctx, sourcePath, destPath); err != nil {
slog.Error("Failed to move blob",
"source", sourcePath,
"dest", destPath,
+2 -5
View File
@@ -12,14 +12,12 @@ import (
"atcr.io/pkg/hold/pds"
"atcr.io/pkg/hold/quota"
"atcr.io/pkg/s3"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// XRPCHandler handles OCI-specific XRPC endpoints for multipart uploads
type XRPCHandler struct {
driver storagedriver.StorageDriver
s3Service s3.S3Service
MultipartMgr *MultipartManager // Exported for access in route handlers
pds *pds.HoldPDS
@@ -30,9 +28,8 @@ type XRPCHandler struct {
}
// NewXRPCHandler creates a new OCI XRPC handler
func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, enableBlueskyPosts bool, httpClient pds.HTTPClient, quotaMgr *quota.Manager) *XRPCHandler {
func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, enableBlueskyPosts bool, httpClient pds.HTTPClient, quotaMgr *quota.Manager) *XRPCHandler {
return &XRPCHandler{
driver: driver,
MultipartMgr: NewMultipartManager(),
s3Service: s3Service,
pds: holdPDS,
@@ -366,7 +363,7 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
postURI, err = h.pds.CreateManifestPost(
ctx,
h.driver,
&h.s3Service,
req.Repository,
req.Tag,
userHandle,
+35 -214
View File
@@ -2,7 +2,6 @@ package oci
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
@@ -10,17 +9,12 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth/oauth"
"atcr.io/pkg/hold/pds"
"atcr.io/pkg/s3"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/registry/storage/driver/factory"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
)
// Shared test resources for OCI package
@@ -68,148 +62,10 @@ func (m *mockPDSClient) Do(req *http.Request) (*http.Response, error) {
}, nil
}
// mockStorageDriver implements storagedriver.StorageDriver for testing
type mockStorageDriver struct {
mu sync.RWMutex
blobs map[string][]byte
// Error injection for testing error handling
StatError error
MoveError error
}
func newMockStorageDriver() *mockStorageDriver {
return &mockStorageDriver{
blobs: make(map[string][]byte),
}
}
func (m *mockStorageDriver) Name() string { return "mock" }
func (m *mockStorageDriver) GetContent(ctx context.Context, path string) ([]byte, error) {
m.mu.RLock()
defer m.mu.RUnlock()
if data, ok := m.blobs[path]; ok {
return data, nil
}
return nil, storagedriver.PathNotFoundError{Path: path}
}
func (m *mockStorageDriver) PutContent(ctx context.Context, path string, content []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
m.blobs[path] = content
return nil
}
func (m *mockStorageDriver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
data, err := m.GetContent(ctx, path)
if err != nil {
return nil, err
}
return io.NopCloser(bytes.NewReader(data[offset:])), nil
}
func (m *mockStorageDriver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) {
return &mockFileWriter{driver: m, path: path}, nil
}
func (m *mockStorageDriver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// Check for injected error
if m.StatError != nil {
return nil, m.StatError
}
if data, ok := m.blobs[path]; ok {
return &mockFileInfo{path: path, size: int64(len(data))}, nil
}
return nil, storagedriver.PathNotFoundError{Path: path}
}
func (m *mockStorageDriver) List(ctx context.Context, path string) ([]string, error) {
return nil, nil
}
func (m *mockStorageDriver) Move(ctx context.Context, sourcePath string, destPath string) error {
m.mu.Lock()
defer m.mu.Unlock()
// Check for injected error
if m.MoveError != nil {
return m.MoveError
}
if data, ok := m.blobs[sourcePath]; ok {
m.blobs[destPath] = data
delete(m.blobs, sourcePath)
return nil
}
return storagedriver.PathNotFoundError{Path: sourcePath}
}
func (m *mockStorageDriver) Delete(ctx context.Context, path string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.blobs, path)
return nil
}
func (m *mockStorageDriver) RedirectURL(r *http.Request, path string) (string, error) {
return "", storagedriver.ErrUnsupportedMethod{}
}
func (m *mockStorageDriver) Walk(ctx context.Context, path string, f storagedriver.WalkFn, options ...func(*storagedriver.WalkOptions)) error {
return nil
}
// mockFileWriter implements storagedriver.FileWriter
type mockFileWriter struct {
driver *mockStorageDriver
path string
buf bytes.Buffer
}
func (w *mockFileWriter) Write(p []byte) (int, error) {
return w.buf.Write(p)
}
func (w *mockFileWriter) Size() int64 {
return int64(w.buf.Len())
}
func (w *mockFileWriter) Close() error {
return nil
}
func (w *mockFileWriter) Cancel(ctx context.Context) error {
return nil
}
func (w *mockFileWriter) Commit(ctx context.Context) error {
w.driver.mu.Lock()
defer w.driver.mu.Unlock()
w.driver.blobs[w.path] = w.buf.Bytes()
return nil
}
// mockFileInfo implements storagedriver.FileInfo
type mockFileInfo struct {
path string
size int64
}
func (f *mockFileInfo) Path() string { return f.path }
func (f *mockFileInfo) Size() int64 { return f.size }
func (f *mockFileInfo) ModTime() time.Time { return time.Time{} }
func (f *mockFileInfo) IsDir() bool { return false }
// setupTestOCIHandlerWithMockS3 creates a test OCI XRPC handler with mock S3
// This does NOT require real S3 credentials - uses MockS3Client
// Returns the handler, mock S3 client, and mock storage driver for test manipulation
func setupTestOCIHandlerWithMockS3(t *testing.T) (*XRPCHandler, *s3.MockS3Client, *mockStorageDriver) {
// Returns the handler and mock S3 client for test manipulation
func setupTestOCIHandlerWithMockS3(t *testing.T) (*XRPCHandler, *s3.MockS3Client) {
t.Helper()
// Create temp directory for PDS database
@@ -226,9 +82,6 @@ func setupTestOCIHandlerWithMockS3(t *testing.T) (*XRPCHandler, *s3.MockS3Client
PathPrefix: "test-prefix",
}
// Create mock storage driver
mockDriver := newMockStorageDriver()
// Create minimal PDS for DID/auth
dbPath := ":memory:"
keyPath := filepath.Join(tmpDir, "signing-key")
@@ -268,9 +121,9 @@ func setupTestOCIHandlerWithMockS3(t *testing.T) (*XRPCHandler, *s3.MockS3Client
mockClient := &mockPDSClient{}
// Create OCI handler with mock S3
handler := NewXRPCHandler(holdPDS, s3Service, mockDriver, false, mockClient, nil)
handler := NewXRPCHandler(holdPDS, s3Service, false, mockClient, nil)
return handler, mockS3Client, mockDriver
return handler, mockS3Client
}
// setupTestOCIHandlerWithS3 creates a test OCI XRPC handler with S3 driver
@@ -307,12 +160,6 @@ func setupTestOCIHandlerWithS3(t *testing.T) (*XRPCHandler, bool) {
}
s3Params["rootdirectory"] = storageDir
driver, err := factory.Create(ctx, "s3", s3Params)
if err != nil {
t.Logf("Failed to create S3 storage driver: %v", err)
return nil, false
}
// Create S3 service
s3Service, err := s3.NewS3Service(s3Params)
if err != nil {
@@ -359,7 +206,7 @@ func setupTestOCIHandlerWithS3(t *testing.T) (*XRPCHandler, bool) {
mockClient := &mockPDSClient{}
// Create OCI handler with S3
handler := NewXRPCHandler(holdPDS, *s3Service, driver, false, mockClient, nil)
handler := NewXRPCHandler(holdPDS, *s3Service, false, mockClient, nil)
return handler, true
}
@@ -392,7 +239,7 @@ func decodeJSONResponse(t *testing.T, w *httptest.ResponseRecorder, v any) {
// Tests for HandleInitiateUpload - Mock S3 (no credentials required)
func TestHandleInitiateUpload_MockS3_Success(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{
"digest": "sha256:abc123",
@@ -421,7 +268,7 @@ func TestHandleInitiateUpload_MockS3_Success(t *testing.T) {
}
func TestHandleInitiateUpload_MockS3_MissingDigest(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{})
addMockAuth(req)
@@ -437,7 +284,7 @@ func TestHandleInitiateUpload_MockS3_MissingDigest(t *testing.T) {
// Tests for full Mock S3 upload flow (no credentials required)
func TestFullMockS3UploadFlow(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// 1. Initiate upload
initReq := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{
@@ -506,7 +353,7 @@ func TestFullMockS3UploadFlow(t *testing.T) {
}
func TestHandleGetPartUploadUrl_MockS3_InvalidSession(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
"uploadId": "invalid-upload-id",
@@ -523,7 +370,7 @@ func TestHandleGetPartUploadUrl_MockS3_InvalidSession(t *testing.T) {
}
func TestHandleAbortUpload_MockS3_InvalidSession(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldAbortUpload, map[string]string{
"uploadId": "invalid-upload-id",
@@ -764,7 +611,7 @@ func TestFullS3UploadFlow(t *testing.T) {
// Tests for HandleCompleteUpload with Mock S3
func TestHandleCompleteUpload_MockS3_Success(t *testing.T) {
handler, mockS3Client, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// 1. Initiate upload with temp path
tempDigest := "uploads/temp-test-complete"
@@ -796,12 +643,8 @@ func TestHandleCompleteUpload_MockS3_Success(t *testing.T) {
t.Fatalf("Expected status 200 for part URL, got %d: %s", partW.Code, partW.Body.String())
}
// 3. Pre-populate mock storage driver with temp blob (simulates S3 upload completing)
// Path format: /docker/registry/v2/uploads/temp-{id}/data
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test blob content"))
// 4. Complete upload with parts
// 3. Complete upload with parts
// (Mock S3 CompleteMultipartUpload auto-populates object for Stat/Move)
finalDigest := "sha256:abc123def456"
completeReq := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
"uploadId": uploadID,
@@ -833,17 +676,15 @@ func TestHandleCompleteUpload_MockS3_Success(t *testing.T) {
t.Errorf("Expected 1 Complete call, got %d", len(mockS3Client.CompleteCalls))
}
// 6. Verify blob was moved to final location
// Final path format: /docker/registry/v2/blobs/sha256/ab/abc123def456/data
finalBlobPath := "/docker/registry/v2/blobs/sha256/ab/abc123def456/data"
_, err := mockDriver.Stat(t.Context(), finalBlobPath)
if err != nil {
t.Errorf("Expected blob at final location %s, got error: %v", finalBlobPath, err)
// 6. Verify blob was moved to final location in mock S3
finalS3Key := "test-prefix/docker/registry/v2/blobs/sha256/ab/abc123def456/data"
if mockS3Client.GetObject(finalS3Key) == nil {
t.Errorf("Expected blob at final S3 key %s", finalS3Key)
}
}
func TestHandleCompleteUpload_MockS3_InvalidSession(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
"uploadId": "non-existent-upload-id",
@@ -863,7 +704,7 @@ func TestHandleCompleteUpload_MockS3_InvalidSession(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_MissingParams(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
tests := []struct {
name string
@@ -891,7 +732,7 @@ func TestHandleCompleteUpload_MockS3_MissingParams(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_ETagNormalization(t *testing.T) {
handler, mockS3Client, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Setup upload session
tempDigest := "uploads/temp-etag-test"
@@ -906,10 +747,6 @@ func TestHandleCompleteUpload_MockS3_ETagNormalization(t *testing.T) {
decodeJSONResponse(t, initW, &initResp)
uploadID := initResp["uploadId"].(string)
// Pre-populate mock driver
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test"))
// Complete with unquoted ETags
completeReq := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
"uploadId": uploadID,
@@ -938,7 +775,7 @@ func TestHandleCompleteUpload_MockS3_ETagNormalization(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_S3Error(t *testing.T) {
handler, mockS3Client, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Setup upload session
tempDigest := "uploads/temp-s3-error"
@@ -953,10 +790,6 @@ func TestHandleCompleteUpload_MockS3_S3Error(t *testing.T) {
decodeJSONResponse(t, initW, &initResp)
uploadID := initResp["uploadId"].(string)
// Pre-populate mock driver
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test"))
// Inject S3 error
mockS3Client.CompleteError = fmt.Errorf("simulated S3 CompleteMultipartUpload failure")
@@ -983,7 +816,7 @@ func TestHandleCompleteUpload_MockS3_S3Error(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_StatError(t *testing.T) {
handler, _, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Setup upload session
tempDigest := "uploads/temp-stat-error"
@@ -998,12 +831,8 @@ func TestHandleCompleteUpload_MockS3_StatError(t *testing.T) {
decodeJSONResponse(t, initW, &initResp)
uploadID := initResp["uploadId"].(string)
// Pre-populate mock driver (so S3 complete succeeds)
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test"))
// Inject Stat error (simulates blob not found after S3 complete)
mockDriver.StatError = fmt.Errorf("simulated stat failure")
// Inject HeadObject error (simulates blob not found after S3 complete)
mockS3Client.HeadObjectError = fmt.Errorf("simulated stat failure")
// Complete upload should fail
completeReq := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
@@ -1023,7 +852,7 @@ func TestHandleCompleteUpload_MockS3_StatError(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_MoveError(t *testing.T) {
handler, _, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Setup upload session
tempDigest := "uploads/temp-move-error"
@@ -1038,12 +867,8 @@ func TestHandleCompleteUpload_MockS3_MoveError(t *testing.T) {
decodeJSONResponse(t, initW, &initResp)
uploadID := initResp["uploadId"].(string)
// Pre-populate mock driver
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test"))
// Inject Move error
mockDriver.MoveError = fmt.Errorf("simulated move failure")
// Inject CopyObject error (Move = Copy + Delete, so Copy error simulates move failure)
mockS3Client.CopyObjectError = fmt.Errorf("simulated move failure")
// Complete upload should fail
completeReq := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
@@ -1068,7 +893,7 @@ func TestHandleCompleteUpload_MockS3_MoveError(t *testing.T) {
}
func TestHandleCompleteUpload_MockS3_UnsortedParts(t *testing.T) {
handler, mockS3Client, mockDriver := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Setup upload session
tempDigest := "uploads/temp-unsorted"
@@ -1083,10 +908,6 @@ func TestHandleCompleteUpload_MockS3_UnsortedParts(t *testing.T) {
decodeJSONResponse(t, initW, &initResp)
uploadID := initResp["uploadId"].(string)
// Pre-populate mock driver
tempBlobPath := fmt.Sprintf("/docker/registry/v2/%s/data", tempDigest)
mockDriver.PutContent(t.Context(), tempBlobPath, []byte("test"))
// Complete with unsorted parts (3, 1, 2) - handler should sort them
completeReq := makeJSONRequest("POST", atproto.HoldCompleteUpload, map[string]any{
"uploadId": uploadID,
@@ -1117,7 +938,7 @@ func TestHandleCompleteUpload_MockS3_UnsortedParts(t *testing.T) {
// Tests for HandleInitiateUpload edge cases
func TestHandleInitiateUpload_MockS3_S3Error(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// Inject S3 error
mockS3Client.CreateMultipartError = fmt.Errorf("simulated S3 CreateMultipartUpload failure")
@@ -1141,7 +962,7 @@ func TestHandleInitiateUpload_MockS3_S3Error(t *testing.T) {
}
func TestHandleInitiateUpload_MockS3_WhitespaceDigest(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{
"digest": " ", // Whitespace only
@@ -1162,7 +983,7 @@ func TestHandleInitiateUpload_MockS3_WhitespaceDigest(t *testing.T) {
// Tests for HandleGetPartUploadURL edge cases
func TestHandleGetPartUploadUrl_MockS3_MissingParams(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
tests := []struct {
name string
@@ -1189,7 +1010,7 @@ func TestHandleGetPartUploadUrl_MockS3_MissingParams(t *testing.T) {
}
func TestHandleGetPartUploadUrl_MockS3_ValidSession(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// First initiate an upload
initReq := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{
@@ -1237,7 +1058,7 @@ func TestHandleGetPartUploadUrl_MockS3_ValidSession(t *testing.T) {
// Tests for HandleAbortUpload edge cases
func TestHandleAbortUpload_MockS3_MissingUploadId(t *testing.T) {
handler, _, _ := setupTestOCIHandlerWithMockS3(t)
handler, _ := setupTestOCIHandlerWithMockS3(t)
req := makeJSONRequest("POST", atproto.HoldAbortUpload, map[string]string{})
addMockAuth(req)
@@ -1251,7 +1072,7 @@ func TestHandleAbortUpload_MockS3_MissingUploadId(t *testing.T) {
}
func TestHandleAbortUpload_MockS3_S3Error(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// First initiate an upload
initReq := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{
@@ -1288,7 +1109,7 @@ func TestHandleAbortUpload_MockS3_S3Error(t *testing.T) {
}
func TestHandleAbortUpload_MockS3_ValidSession(t *testing.T) {
handler, mockS3Client, _ := setupTestOCIHandlerWithMockS3(t)
handler, mockS3Client := setupTestOCIHandlerWithMockS3(t)
// First initiate an upload
initReq := makeJSONRequest("POST", atproto.HoldInitiateUpload, map[string]string{