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
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.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.