Plumb a per-resource owner (Zitadel sub) through the sync contract without enforcing isolation client-side — the server stays the authority. - Dtos: add optional ownerId to RemoteList/RemoteTask/MirrorTask - JwtClaims: decode the sub claim from the access token (never throws) - OnlineSyncService: stamp ownerId on pushed lists + mirror; defensively skip pulled tasks owned by a different user (unowned tasks still sync, so single-user behavior is unchanged) - docs: contract documents ownerId + multi-user readiness Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 lines
1.1 KiB
C#
41 lines
1.1 KiB
C#
using System.Text;
|
|
using ClaudeDo.Worker.Online;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Online;
|
|
|
|
public sealed class JwtClaimsTests
|
|
{
|
|
// Builds a fake JWT (header.payload.signature) carrying the given JSON payload.
|
|
internal static string MakeToken(string payloadJson)
|
|
{
|
|
static string B64Url(string s) =>
|
|
Convert.ToBase64String(Encoding.UTF8.GetBytes(s))
|
|
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
return $"{B64Url("{\"alg\":\"RS256\"}")}.{B64Url(payloadJson)}.sig";
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSubject_ReturnsSub()
|
|
{
|
|
var token = MakeToken("{\"sub\":\"user-123\",\"email\":\"a@b.c\"}");
|
|
Assert.Equal("user-123", JwtClaims.GetSubject(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSubject_Null_WhenNoSub()
|
|
{
|
|
var token = MakeToken("{\"email\":\"a@b.c\"}");
|
|
Assert.Null(JwtClaims.GetSubject(token));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("not-a-jwt")]
|
|
[InlineData("opaque.token")]
|
|
public void GetSubject_Null_ForUnparseableInput(string? token)
|
|
{
|
|
Assert.Null(JwtClaims.GetSubject(token));
|
|
}
|
|
}
|