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"); } /// /// Ein Sub-Token darf keine weiteren ausstellen. Das ist der Regelfall beim zweiten /// Start und muss als sauberer Fehler herauskommen, nicht als Absturz. /// [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) ─── /// /// Die Auskunft, auf die es ankommt: an welches Projekt das Token gebunden ist. /// /// Seit SDK 2.8.1 erbt ein Watchdog-Monitor das Projekt seines Ping-Tokens. /// Fehlt die Bindung, landen die Befunde im Scheinprojekt „default" — und eine /// Abfrage nach dem eigenen Projekt liefert für eine laufende, meldende Instanz null /// Befunde. Die Aufsicht sähe aus wie „nichts auffällig". /// [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 ist JSON-null, nicht abwesend - genau so kommt es vom 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() { // Ein aelterer Server kennt whoami nicht in dieser Form. Die Auskunft darf // ausfallen; abstuerzen darf sie nicht. 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(); } }