using System.Net; using ClawdDotNet.Core.Deploymentcenter; using Shouldly; namespace ClawdDotNet.Core.Tests.Deploymentcenter; public sealed class TokenProvisionerTests { [Fact] public async Task Instanz_bekommt_ein_eigenes_Sub_Token() { var handler = new CapturingHandler(HttpStatusCode.Created, """ {"status":"success","sub_token":"dc_sub_abc","token_id":"tok_s_1", "scopes":["watchdog:ping","bugtracker:report"]} """); using var api = new DeploymentcenterApi("https://dc.example", "master", new HttpClient(handler)); var token = await new TokenProvisioner(api).ProvisionAsync( "ClawdDotNet Produktion", "inst-1", TokenProvisioner.InstanceScopes); handler.LastRequest!.RequestUri!.ToString().ShouldBe("https://dc.example/api/tokens/v1/provision"); handler.LastRequest.Headers.Authorization!.ToString().ShouldBe("Bearer master"); handler.LastBody!.ShouldContain("\"instance_id\":\"inst-1\""); handler.LastBody.ShouldNotBeNull().ShouldContain("watchdog:ping"); token.Token.ShouldBe("dc_sub_abc"); token.TokenId.ShouldBe("tok_s_1"); token.Scopes.ShouldContain("bugtracker:report"); } /// /// A sub-token may not issue further ones. That is the normal case at the second /// start and must come out as a clean error, not as a crash. /// [Fact] public async Task Abgelehnte_Provisionierung_wirft_mit_Code() { var handler = new CapturingHandler(HttpStatusCode.Forbidden, """{"status":"error","error":{"code":"provision_denied","message":"Kein Master-Token."}}"""); using var api = new DeploymentcenterApi("https://dc.example", "sub", new HttpClient(handler)); var ex = await Should.ThrowAsync( () => new TokenProvisioner(api).ProvisionAsync("x", "inst-1", ["watchdog:ping"])); ex.Code.ShouldBe("provision_denied"); } // ─── whoami (SDK 2.7.1) ─── /// /// The information that matters: which project the token is bound to. /// /// Since SDK 2.8.1 a watchdog monitor inherits the project of its ping token. /// If the binding is missing, the findings land in the phantom project "default" — and a /// query for the own project returns zero findings for a running, reporting instance. /// Supervision would look like "nothing unusual". /// [Fact] public async Task Whoami_liest_die_Projektbindung() { var handler = new CapturingHandler(HttpStatusCode.OK, """ {"status":"success","token_id":"tok_m_1","name":"DEV-ClawdDotNet","type":"master", "owner_type":"dev_agent","is_workbench":true,"project_slug":"clawddotnet", "environment":"all","scopes":["*"]} """); using var api = new DeploymentcenterApi("https://dc.example", "master", new HttpClient(handler)); var identity = await new TokenProvisioner(api).WhoAmIAsync(); handler.LastRequest!.RequestUri!.ToString().ShouldBe("https://dc.example/api/tokens/v1/whoami"); handler.LastRequest.Method.ShouldBe(HttpMethod.Get); identity.TokenId.ShouldBe("tok_m_1"); identity.Name.ShouldBe("DEV-ClawdDotNet"); identity.Type.ShouldBe("master"); identity.ProjectSlug.ShouldBe("clawddotnet"); identity.IsProjectBound.ShouldBeTrue(); identity.Scopes.ShouldContain("*"); } [Fact] public async Task Ein_Token_ohne_Projekt_faellt_auf() { // project_slug is JSON null, not absent - exactly as it comes from the server. var handler = new CapturingHandler(HttpStatusCode.OK, """ {"status":"success","token_id":"tok_s_9","name":"Instanz","type":"sub", "project_slug":null,"environment":"production","scopes":["watchdog:ping"]} """); using var api = new DeploymentcenterApi("https://dc.example", "sub", new HttpClient(handler)); var identity = await new TokenProvisioner(api).WhoAmIAsync(); identity.ProjectSlug.ShouldBeNull(); identity.IsProjectBound.ShouldBeFalse(); } [Fact] public async Task Eine_Antwort_ohne_Felder_wirft_nicht() { // An older server does not know whoami in this form. The information may // be missing; it must not crash. var handler = new CapturingHandler(HttpStatusCode.OK, """{"status":"success"}"""); using var api = new DeploymentcenterApi("https://dc.example", "t", new HttpClient(handler)); var identity = await new TokenProvisioner(api).WhoAmIAsync(); identity.TokenId.ShouldBe(""); identity.Type.ShouldBe("unknown"); identity.IsProjectBound.ShouldBeFalse(); identity.Scopes.ShouldBeEmpty(); } }