File Upload End-to-End — Cover Image and Avatar Full Flow

End-to-end verification of the file upload feature confirms that data flows correctly through every layer and that the image appears where expected throughout the BlogApp. Two complete flows are verified: the avatar upload (affects the navbar and profile display) and the cover image upload in the post form (affects post cards and the detail page). Both flows must work with authentication and handle errors gracefully.

Complete Upload Flow Verification

// ── Avatar upload flow — ProfileComponent ─────────────────────────────────
@Component({
  selector:   'app-profile',
  standalone:  true,
  imports:    [ImageUploadComponent, ReactiveFormsModule, MatFormFieldModule,
               MatInputModule, MatButtonModule],
  template: `
    <div class="profile-page">
      <h1>Your Profile</h1>

      <!-- Avatar upload — feeds into AuthService ─────────────────────── -->
      <section class="avatar-section">
        <h2>Profile Photo</h2>
        <app-image-upload
          [maxSizeMb]="2"
          (uploaded)="onAvatarUploaded($event)">
        </app-image-upload>
        <!-- After upload: (uploaded) fires with CDN URL → API PUT /api/users/me/avatar -->
        <!-- → AuthService updates currentUser signal → navbar avatar updates reactively -->
      </section>

      <!-- Profile form ─────────────────────────────────────────────────── -->
      <form [formGroup]="profileForm" (ngSubmit)="onSave()">
        <mat-form-field>
          <mat-label>Display Name</mat-label>
          <input matInput formControlName="displayName">
        </mat-form-field>
        <mat-form-field>
          <mat-label>Bio</mat-label>
          <textarea matInput formControlName="bio" rows="4"></textarea>
        </mat-form-field>
        <button mat-raised-button color="primary" type="submit">Save</button>
      </form>
    </div>
  `,
})
export class ProfileComponent {
  private usersApi   = inject(UsersApiService);
  private auth       = inject(AuthService);
  private notify     = inject(NotificationService);
  private destroyRef = inject(DestroyRef);

  profileForm = inject(FormBuilder).nonNullable.group({
    displayName: [this.auth.displayName(), Validators.required],
    bio:         [''],
  });

  onAvatarUploaded(url: string): void {
    // Store the URL via API, then refresh the auth state
    this.usersApi.updateAvatar(url).pipe(
      takeUntilDestroyed(this.destroyRef),
    ).subscribe(() => {
      this.auth.updateCurrentUserAvatar(url);  // update in-memory claims
      this.notify.success('Profile photo updated!');
    });
  }

  onSave(): void {
    // ... standard form save
  }
}

End-to-End Verification Checklist

-- ── Avatar upload flow ─────────────────────────────────────────────────────
-- 1. NAVIGATE to /profile while authenticated
-- 2. DRAG an image onto the upload zone
--    → Client validates: correct type, under 2MB
--    → Preview appears immediately (FileReader data URL)
-- 3. UPLOAD starts:
--    Network tab: POST /api/uploads/images
--    Request: multipart/form-data with Authorization: Bearer token
--    Progress bar: 0% → 100%
--    Response: { "url": "https://cdn.blogapp.com/images/abc123.webp" }
-- 4. CALLBACK: onAvatarUploaded fires with the CDN URL
-- 5. API CALL: PUT /api/users/me/avatar with the URL
--    Response: 200 OK
-- 6. UI UPDATE: navbar avatar changes without page reload
--    (auth.currentUser() signal updated → navbar reactive to signal)

-- ── Cover image upload in post form ───────────────────────────────────────
-- 1. NAVIGATE to /admin/posts/new (requires Admin or Author role)
-- 2. FILL in title, slug, body
-- 3. UPLOAD cover image via ImageUploadComponent in the form
--    → Preview shows in the upload zone
--    → ImageUploadComponent emits (uploaded) with the CDN URL
--    → PostFormComponent stores the URL in a form field
-- 4. PUBLISH the post
-- 5. VERIFY on post list: cover image displays on the PostCardComponent
--    → CdnUrlPipe transforms the URL if needed
--    → loading="lazy" attribute present (check Elements tab in DevTools)
--    → width and height attributes present (verify no CLS in Lighthouse)
-- 6. VERIFY on post detail: cover image displays as the hero image
--    → loading="eager" on the first visible image (verify in Elements tab)

-- ── Error cases ───────────────────────────────────────────────────────────
-- Test: upload a non-image file (.txt, .pdf)
-- Expected: client validation shows "Only image files are allowed." immediately
-- Network tab: no request sent (client-side rejection)

-- Test: upload a 6MB image
-- Expected: client shows "Image must be under 5MB." immediately

-- Test: upload while unauthenticated (clear token in debug overlay)
-- Expected: POST /api/uploads/images → 401 → auth interceptor redirects to login

-- Test: upload a valid image from a non-image file (spoofed Content-Type)
-- Expected: server magic byte check rejects with 400 Bad Request
Note: The avatar update triggers a reactive update in the navbar without a page reload because the AuthService.currentUser() is a Signal. The navbar binds to auth.currentUser()?.avatarUrl — when the updateCurrentUserAvatar(url) method updates the signal’s value, Angular’s change detection automatically re-renders the navbar avatar. This is the reactive programming model in action: data flows from the upload → auth service signal → UI binding, with no explicit refresh calls needed.
Tip: Run Google Lighthouse (DevTools → Lighthouse tab) on the post list and detail pages after implementing the image display. Lighthouse measures Core Web Vitals including LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), and FID (First Input Delay). Common issues: missing width/height causing CLS, loading=”lazy” on the LCP image, images not served from CDN (high TTFB), and uncompressed images. Lighthouse provides specific actionable recommendations for each issue found.
Warning: The ImageUploadComponent sends the upload request with withCredentials: true for the auth cookie. For Azure Blob Storage’s CORS configuration, ensure https://blogapp.com and https://api.blogapp.com are in the allowed origins list on the storage account. If CORS is not configured on the storage account, direct browser-to-blob requests fail. Since uploads go through the API (not directly to blob storage), CORS on the storage account is only needed if you implement direct browser-to-blob uploads (which require SAS tokens).

Common Mistakes

Mistake 1 — Not updating the auth service after avatar upload (navbar shows old avatar)

❌ Wrong — API call to update avatar succeeds; auth service signal not updated; navbar still shows old avatar.

✅ Correct — call auth.updateCurrentUserAvatar(url) after the API responds; signal update propagates to all UI.

Mistake 2 — Cover image URL stored as null in form but post saved anyway

❌ Wrong — user uploads image but form group does not capture the URL from the (uploaded) event; post saved with null cover.

✅ Correct — wire (uploaded)="form.patchValue({ coverImageUrl: $event })" on the ImageUploadComponent in the post form.

🧠 Test Yourself

The post form has an ImageUploadComponent with (uploaded)="form.patchValue({ coverImageUrl: $event })". The user selects an image and sees the preview. They then change their mind and navigate away before the upload completes. What happens?