BlogApiClient is a thin DTO↔path mapper on top of YavscApiClient:
- No more HttpClient, no more accessToken constructor argument.
- Single responsibility: turn Yavsc.Blogs endpoint paths into typed
BlogPost payloads and back, while YavscApiClient owns auth +
refresh + JSON shape.
- Default path prefix is 'api/blog', overridable for tests.
MainPageViewModel and App.axaml.cs are now free of any direct
OidcClient / IBrowser / BearerToken plumbing. The MainPage receives
a fully-configured BlogApiClient (which holds a YavscApiClient, which
holds a TokenStore) at construction. The duplicate LoginAsync method
on MainPageViewModel is gone; the LoginPage is the single entry point
for the interactive PKCE flow.
PlatformBootstrap.Desktop no longer overrides the redirect URI to
loopback. PostIt runs the postit://callback custom scheme
(SingleInstance hand-off) as the production path on desktop; the
loopback constant stays for tests and for platforms that cannot
register a custom scheme.
Settings.cs: DefaultLoopbackRedirectUri is now documented as a
fallback; DefaultDesktopRedirectUri ('postit://callback') is
introduced as the canonical desktop default.
TokenStore.Load() tolerates an empty file (returns null) so
first-launch races and stubbed test fixtures don't blow up the
constructor.
YavscApiClient:
- HasValidSession is exposed for warm-start UI logic.
- CurrentAccessToken / CurrentIdToken are exposed so the LoginPage
ViewModel can mirror the result onto its observable properties.
- CallAsync<T> is now virtual (and the class is no longer sealed)
to allow stubbing in PostItViewModelTests.
Tests (PostIt.Tests):
- YavscApiClientTests covers the silent refresh path (cache the
token, mark it expired, observe a new Bearer in the API server),
the 401 → refresh → retry path (forceFirstRequest on the stub),
HasValidSession after login, and the throw-when-no-token guard.
- OidcStubAuthority now mints a refresh_token in the token response
so YavscApiClient.RefreshTokenAsync can hit /connect/token in
tests.
- PostItViewModelTests uses a ThrowingYavscApiClient / StubYavscApiClient
pair instead of the old HttpClient injection point, matching the
new constructor shape.
dotnet test: 21/21 green. dotnet build: 0 errors.
53 lines
2.1 KiB
C#
53 lines
2.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using PostIt.Models;
|
|
|
|
namespace PostIt.Services;
|
|
|
|
/// <summary>
|
|
/// High-level client for the Blog subsystem of the Yavsc API
|
|
/// (deployed at <c>https://blogs.pschneider.fr</c>). All transport
|
|
/// concerns — base URL, JSON serialisation, Bearer auth, silent
|
|
/// refresh on 401, request body shaping — are delegated to
|
|
/// <see cref="YavscApiClient"/>. This class is a thin DTO↔path
|
|
/// mapper, nothing more.
|
|
///
|
|
/// The class is intentionally non-IDisposable: it does not own the
|
|
/// <see cref="YavscApiClient"/> it depends on. Lifetimes are managed
|
|
/// by the consumer (typically a singleton service registered with
|
|
/// the application).
|
|
/// </summary>
|
|
public sealed class BlogApiClient
|
|
{
|
|
private const string DefaultPathPrefix = "api/blog";
|
|
|
|
private readonly YavscApiClient _api;
|
|
private readonly string _pathPrefix;
|
|
|
|
public BlogApiClient(YavscApiClient api, string pathPrefix = DefaultPathPrefix)
|
|
{
|
|
_api = api ?? throw new ArgumentNullException(nameof(api));
|
|
_pathPrefix = pathPrefix?.TrimStart('/') ?? DefaultPathPrefix;
|
|
}
|
|
|
|
public Task<List<BlogPost>> GetPostsAsync(int start = 0, int take = 25, CancellationToken ct = default)
|
|
=> _api.CallAsync<List<BlogPost>>(
|
|
HttpMethod.Get,
|
|
$"{_pathPrefix}?start={start}&take={take}",
|
|
ct: ct);
|
|
|
|
public Task<BlogPost?> GetPostAsync(long id, CancellationToken ct = default)
|
|
=> _api.CallAsync<BlogPost?>(HttpMethod.Get, $"{_pathPrefix}/{id}", ct: ct);
|
|
|
|
public Task<BlogPost?> CreatePostAsync(BlogPost post, CancellationToken ct = default)
|
|
=> _api.CallAsync<BlogPost?>(HttpMethod.Post, _pathPrefix, body: post, ct: ct);
|
|
|
|
public Task UpdatePostAsync(long id, BlogPost post, CancellationToken ct = default)
|
|
=> _api.CallAsync(HttpMethod.Put, $"{_pathPrefix}/{id}", body: post, ct: ct);
|
|
|
|
public Task DeletePostAsync(long id, CancellationToken ct = default)
|
|
=> _api.CallAsync(HttpMethod.Delete, $"{_pathPrefix}/{id}", ct: ct);
|
|
}
|