0.3.0: UGC removed, battlepass context-carrying API, scenario behavior aligned with web SDK, CI publish
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build Rudder.Core.csproj -c Release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test tests/Rudder.Core.Tests/Rudder.Core.Tests.csproj -c Release
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: dotnet pack Rudder.Core.csproj -c Release -o artifacts
|
||||||
|
|
||||||
|
- name: Add Gitea NuGet source
|
||||||
|
run: dotnet nuget add source --name gitea --username ${{ github.actor }} --password ${{ secrets.GITEA_TOKEN }} --store-password-in-clear-text https://hub.rudder.build/api/packages/rudder/nuget/index.json
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: dotnet nuget push artifacts/*.nupkg --source gitea
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uploads raw bytes to pre-signed UGC URLs. Optional; required only by
|
|
||||||
/// <see cref="UgcService.UploadAsync"/>.
|
|
||||||
/// </summary>
|
|
||||||
public interface IUploadTransport
|
|
||||||
{
|
|
||||||
/// <summary>PUTs the payload to the pre-signed URL with the given content type.</summary>
|
|
||||||
Task PutAsync(string url, byte[] data, string contentType, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,40 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- UGC surface: `UgcService` (`client.Ugc`), the generated
|
||||||
|
`RudderSdk.Core.Models.Ugc` DTOs, `IUploadTransport` and
|
||||||
|
`RudderClientOptions.UploadTransport`. The server no longer exposes the UGC
|
||||||
|
endpoints.
|
||||||
|
|
||||||
|
### Changed (breaking)
|
||||||
|
|
||||||
|
- `BattlePassService` simple overloads sent empty scenario/node ids and always
|
||||||
|
failed server-side. Battle pass state is tied to a scenario battle pass
|
||||||
|
node, so the public API now mirrors the web SDK: `GetProgressAsync(scenarioId,
|
||||||
|
nodeId)` and the request-DTO overloads `AddXpAsync`, `ClaimRewardAsync`,
|
||||||
|
`PurchasePremiumAsync` (carrying `ScenarioId`/`NodeId`/`RunId`) are public;
|
||||||
|
the internal request overloads are gone.
|
||||||
|
- `BattlePassSession` now exposes the bound battle pass operations
|
||||||
|
(`GetProgressAsync`, `AddXpAsync(source, amount)`, `ClaimRewardAsync(level,
|
||||||
|
track)`, `PurchasePremiumAsync()` — crosses `onPremiumPurchase` on success),
|
||||||
|
mirroring the web SDK session.
|
||||||
|
- `BattlePassLevelSession.CompleteAsync`/`Complete` renamed to
|
||||||
|
`ClaimAsync`/`Claim`; new `Level` property reads the `levelNumber` node data
|
||||||
|
key.
|
||||||
|
- Unsupported scenario node types now fail the run (`OnScenarioFailed`)
|
||||||
|
instead of stalling silently.
|
||||||
|
- Scenario counter updates no longer read a continuation plan from the
|
||||||
|
response (the server stopped returning one); when the server reports the
|
||||||
|
objective completed, the node crosses `onComplete`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- A failed scenario counter update no longer fails the run; the error is
|
||||||
|
logged and the node stays active.
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.NET client SDK for the Rudder LiveOps platform: authentication, player
|
.NET client SDK for the Rudder LiveOps platform: authentication, player
|
||||||
profile, stores, battle pass, quests, leaderboards, inventory, remote config,
|
profile, stores, battle pass, quests, leaderboards, inventory, remote config,
|
||||||
scenarios, storage, UGC and realtime.
|
scenarios, storage and realtime.
|
||||||
|
|
||||||
- Target framework: `netstandard2.1` (works in Unity, .NET, Xamarin).
|
- Target framework: `netstandard2.1` (works in Unity, .NET, Xamarin).
|
||||||
- JSON: Newtonsoft.Json.
|
- JSON: Newtonsoft.Json.
|
||||||
@@ -60,7 +60,6 @@ stack.
|
|||||||
| `RemoteConfig` | `RemoteConfigService` | `LoadAsync`, `Get<T>`, `GetAsync<T>` |
|
| `RemoteConfig` | `RemoteConfigService` | `LoadAsync`, `Get<T>`, `GetAsync<T>` |
|
||||||
| `Scenario` | `ScenarioService` | `TriggerAsync`, `RestoreAsync`, `On*` effect events |
|
| `Scenario` | `ScenarioService` | `TriggerAsync`, `RestoreAsync`, `On*` effect events |
|
||||||
| `Storage` | `StorageService` | `GetAsync`, `ListAllAsync`, `SaveAsync`, `DeleteAsync` |
|
| `Storage` | `StorageService` | `GetAsync`, `ListAllAsync`, `SaveAsync`, `DeleteAsync` |
|
||||||
| `Ugc` | `UgcService` | `UploadAsync`, `ListAsync`, `ListAllAsync`, `GetDownloadUrlAsync` |
|
|
||||||
| `Realtime` | `RealtimeService` | `ConnectAsync`, `DisconnectAsync` |
|
| `Realtime` | `RealtimeService` | `ConnectAsync`, `DisconnectAsync` |
|
||||||
|
|
||||||
## Sessions
|
## Sessions
|
||||||
|
|||||||
+2
-2
@@ -6,9 +6,9 @@
|
|||||||
<AssemblyName>Rudder.Core</AssemblyName>
|
<AssemblyName>Rudder.Core</AssemblyName>
|
||||||
<RootNamespace>RudderSdk.Core</RootNamespace>
|
<RootNamespace>RudderSdk.Core</RootNamespace>
|
||||||
<PackageId>Rudder.Core</PackageId>
|
<PackageId>Rudder.Core</PackageId>
|
||||||
<Version>0.2.0</Version>
|
<Version>0.3.0</Version>
|
||||||
<Authors>Rudder</Authors>
|
<Authors>Rudder</Authors>
|
||||||
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios, storage, UGC and realtime.</Description>
|
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios, storage and realtime.</Description>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ public sealed class RudderClient
|
|||||||
/// <summary>Global quests.</summary>
|
/// <summary>Global quests.</summary>
|
||||||
public QuestsService Quests { get; }
|
public QuestsService Quests { get; }
|
||||||
|
|
||||||
/// <summary>User-generated content.</summary>
|
|
||||||
public UgcService Ugc { get; }
|
|
||||||
|
|
||||||
/// <summary>Scenario runtime: triggers, node sessions, persistence.</summary>
|
/// <summary>Scenario runtime: triggers, node sessions, persistence.</summary>
|
||||||
public ScenarioService Scenario { get; }
|
public ScenarioService Scenario { get; }
|
||||||
|
|
||||||
@@ -93,7 +90,6 @@ public sealed class RudderClient
|
|||||||
Stores = new StoresService(this);
|
Stores = new StoresService(this);
|
||||||
Leaderboards = new LeaderboardsService(this);
|
Leaderboards = new LeaderboardsService(this);
|
||||||
Inventory = new InventoryService(this);
|
Inventory = new InventoryService(this);
|
||||||
Ugc = new UgcService(this);
|
|
||||||
Scenario = new ScenarioService(this);
|
Scenario = new ScenarioService(this);
|
||||||
BattlePass = new BattlePassService(this);
|
BattlePass = new BattlePassService(this);
|
||||||
Quests = new QuestsService(this);
|
Quests = new QuestsService(this);
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ public sealed class RudderClientOptions
|
|||||||
/// <summary>HTTP transport. Defaults to <see cref="HttpClientTransport"/>.</summary>
|
/// <summary>HTTP transport. Defaults to <see cref="HttpClientTransport"/>.</summary>
|
||||||
public IRudderTransport? Transport { get; set; }
|
public IRudderTransport? Transport { get; set; }
|
||||||
|
|
||||||
/// <summary>Upload transport for UGC file uploads. Optional.</summary>
|
|
||||||
public IUploadTransport? UploadTransport { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Session token storage. Defaults to <see cref="InMemoryTokenStore"/>.</summary>
|
/// <summary>Session token storage. Defaults to <see cref="InMemoryTokenStore"/>.</summary>
|
||||||
public ITokenStore? TokenStore { get; set; }
|
public ITokenStore? TokenStore { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ using RudderSdk.Core.Models.BattlePass;
|
|||||||
namespace RudderSdk.Core;
|
namespace RudderSdk.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Battle pass progress and rewards. The simple overloads target the project's
|
/// Battle pass progress and rewards. Battle pass state is tied to a scenario
|
||||||
/// default battle pass; the request-based overloads carry the scenario node
|
/// battle pass node, so every call carries the scenario/node ids (and a run id
|
||||||
/// context and are used by the scenario runtime.
|
/// for the mutating calls) — <see cref="BattlePassSession"/> supplies them
|
||||||
|
/// during scenario runs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BattlePassService
|
public sealed class BattlePassService
|
||||||
{
|
{
|
||||||
/// <summary>Free reward track, see <see cref="ClaimRewardAsync(int, string, CancellationToken)"/>.</summary>
|
/// <summary>Free reward track, see <see cref="ClaimRewardAsync(ClaimBattlePassRewardRequest, CancellationToken)"/>.</summary>
|
||||||
public const string TrackFree = "free";
|
public const string TrackFree = "free";
|
||||||
|
|
||||||
/// <summary>Premium reward track, see <see cref="ClaimRewardAsync(int, string, CancellationToken)"/>.</summary>
|
/// <summary>Premium reward track, see <see cref="ClaimRewardAsync(ClaimBattlePassRewardRequest, CancellationToken)"/>.</summary>
|
||||||
public const string TrackPremium = "premium";
|
public const string TrackPremium = "premium";
|
||||||
|
|
||||||
private readonly RudderClient _client;
|
private readonly RudderClient _client;
|
||||||
@@ -23,55 +24,43 @@ public sealed class BattlePassService
|
|||||||
internal BattlePassService(RudderClient client) => _client = client;
|
internal BattlePassService(RudderClient client) => _client = client;
|
||||||
|
|
||||||
/// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary>
|
/// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary>
|
||||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken cancellationToken = default)
|
||||||
=> GetProgressAsync(new GetBattlePassProgressRequest(), cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
|
|
||||||
public Task<AddBattlePassXpResponse> AddXpAsync(long amount, CancellationToken cancellationToken = default)
|
|
||||||
=> AddXpAsync(new AddBattlePassXpRequest { Amount = amount }, cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Claims a tier reward at a reached level (idempotent server-side).
|
|
||||||
/// <paramref name="track"/> is <see cref="TrackFree"/> or <see cref="TrackPremium"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
|
||||||
=> ClaimRewardAsync(new ClaimBattlePassRewardRequest { Level = level, Track = track }, cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Purchases the premium track (charges the wallet). When
|
|
||||||
/// <paramref name="idempotencyKey"/> is null, a random one is generated.
|
|
||||||
/// </summary>
|
|
||||||
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(string? idempotencyKey = null, CancellationToken cancellationToken = default)
|
|
||||||
=> PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
|
||||||
{
|
|
||||||
IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString()
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
internal Task<GetBattlePassProgressResponse> GetProgressAsync(GetBattlePassProgressRequest request, CancellationToken cancellationToken = default)
|
|
||||||
=> _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>(
|
=> _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>(
|
||||||
"POST",
|
"POST",
|
||||||
"/sdk/v1/battlepass/progress",
|
"/sdk/v1/battlepass/progress",
|
||||||
request,
|
new GetBattlePassProgressRequest { ScenarioId = scenarioId, NodeId = nodeId },
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
internal Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken cancellationToken = default)
|
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
|
||||||
|
public Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken cancellationToken = default)
|
||||||
=> _client.SendAsync<AddBattlePassXpRequest, AddBattlePassXpResponse>(
|
=> _client.SendAsync<AddBattlePassXpRequest, AddBattlePassXpResponse>(
|
||||||
"POST",
|
"POST",
|
||||||
"/sdk/v1/battlepass/xp",
|
"/sdk/v1/battlepass/xp",
|
||||||
request,
|
request,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
internal Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken cancellationToken = default)
|
/// <summary>
|
||||||
|
/// Claims a tier reward at a reached level (idempotent server-side).
|
||||||
|
/// <see cref="ClaimBattlePassRewardRequest.Track"/> is <see cref="TrackFree"/> or <see cref="TrackPremium"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken cancellationToken = default)
|
||||||
=> _client.SendAsync<ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse>(
|
=> _client.SendAsync<ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse>(
|
||||||
"POST",
|
"POST",
|
||||||
"/sdk/v1/battlepass/claim",
|
"/sdk/v1/battlepass/claim",
|
||||||
request,
|
request,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
internal Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken cancellationToken = default)
|
/// <summary>
|
||||||
=> _client.SendAsync<PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse>(
|
/// Purchases the premium track (charges the wallet). When
|
||||||
|
/// <see cref="PurchaseBattlePassPremiumRequest.IdempotencyKey"/> is null, a random one is generated.
|
||||||
|
/// </summary>
|
||||||
|
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
request.IdempotencyKey ??= Guid.NewGuid().ToString();
|
||||||
|
return _client.SendAsync<PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse>(
|
||||||
"POST",
|
"POST",
|
||||||
"/sdk/v1/battlepass/premium",
|
"/sdk/v1/battlepass/premium",
|
||||||
request,
|
request,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,12 +227,16 @@ public sealed partial class ScenarioService
|
|||||||
},
|
},
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (response?.Plan != null)
|
// The server reports objective completion; it no longer returns a plan from the
|
||||||
StartPlan(response.Plan);
|
// counter endpoint. On completion, cross the node's onComplete handle (which
|
||||||
|
// advances the run) — idempotent if the consumer also completes the session.
|
||||||
|
if (response != null && response.Completed)
|
||||||
|
await CompleteNodeAsync(runId, nodeId, "onComplete", cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
FailRun(run, nodeId, ex);
|
// Counter update failure does not fail the run.
|
||||||
|
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Scenario counter update failed at node {nodeId}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ public sealed partial class ScenarioService
|
|||||||
EmitBattlePassLevel(context);
|
EmitBattlePassLevel(context);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// Unsupported node type — fail the run (surfaced via OnScenarioFailed)
|
||||||
|
// instead of leaving it stalled on a node no handler will complete.
|
||||||
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
|
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
|
||||||
|
FailRun(run, state.NodeId, new Exception($"Unsupported scenario node type '{node.Type}'"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +127,7 @@ public sealed partial class ScenarioService
|
|||||||
|
|
||||||
private void EmitBattlePass(ScenarioNodeContext context)
|
private void EmitBattlePass(ScenarioNodeContext context)
|
||||||
{
|
{
|
||||||
OnBattlePass?.Invoke(new BattlePassSession(context));
|
OnBattlePass?.Invoke(new BattlePassSession(context, _client.BattlePass));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmitBattlePassLevel(ScenarioNodeContext context)
|
private void EmitBattlePassLevel(ScenarioNodeContext context)
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace RudderSdk.Core;
|
namespace RudderSdk.Core;
|
||||||
|
|
||||||
/// <summary>Session of a scenario battle-pass-level node.</summary>
|
/// <summary>
|
||||||
|
/// Session of a scenario battle-pass-level node — a single claimable tier.
|
||||||
|
/// <see cref="ClaimAsync"/> crosses onComplete, which the server accepts only
|
||||||
|
/// once the player has reached the node's configured level.
|
||||||
|
/// </summary>
|
||||||
public sealed class BattlePassLevelSession
|
public sealed class BattlePassLevelSession
|
||||||
{
|
{
|
||||||
internal BattlePassLevelSession(ScenarioNodeContext context) => Context = context;
|
internal BattlePassLevelSession(ScenarioNodeContext context) => Context = context;
|
||||||
@@ -14,12 +18,15 @@ public sealed class BattlePassLevelSession
|
|||||||
/// <summary>Node id.</summary>
|
/// <summary>Node id.</summary>
|
||||||
public string Id => Context.NodeId;
|
public string Id => Context.NodeId;
|
||||||
|
|
||||||
|
/// <summary>The tier level this node claims.</summary>
|
||||||
|
public int Level => Context.Get("levelNumber", 0);
|
||||||
|
|
||||||
/// <summary>Reads a typed value from the node data.</summary>
|
/// <summary>Reads a typed value from the node data.</summary>
|
||||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||||
|
|
||||||
/// <summary>Advances the run through the onComplete handle.</summary>
|
/// <summary>Claims this tier; crosses onComplete (the server checks the level was reached).</summary>
|
||||||
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
public Task ClaimAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||||
|
|
||||||
/// <summary>Advances the run through the onComplete handle (fire-and-forget).</summary>
|
/// <summary>Claims this tier (fire-and-forget).</summary>
|
||||||
public void Complete() => Context.Complete("onComplete");
|
public void Claim() => Context.Complete("onComplete");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using RudderSdk.Core.Models.BattlePass;
|
||||||
|
|
||||||
namespace RudderSdk.Core;
|
namespace RudderSdk.Core;
|
||||||
|
|
||||||
/// <summary>Session of a scenario battle-pass node.</summary>
|
/// <summary>
|
||||||
|
/// Session of a scenario battle-pass node. Exposes the battle pass operations
|
||||||
|
/// bound to this node's scenario/node/run ids, plus explicit boundary crossings
|
||||||
|
/// the game drives from its UI; the server validates each crossing.
|
||||||
|
/// </summary>
|
||||||
public sealed class BattlePassSession
|
public sealed class BattlePassSession
|
||||||
{
|
{
|
||||||
internal BattlePassSession(ScenarioNodeContext context) => Context = context;
|
private readonly BattlePassService _battlePass;
|
||||||
|
|
||||||
|
internal BattlePassSession(ScenarioNodeContext context, BattlePassService battlePass)
|
||||||
|
{
|
||||||
|
Context = context;
|
||||||
|
_battlePass = battlePass;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Underlying node context.</summary>
|
/// <summary>Underlying node context.</summary>
|
||||||
public ScenarioNodeContext Context { get; }
|
public ScenarioNodeContext Context { get; }
|
||||||
@@ -17,6 +29,52 @@ public sealed class BattlePassSession
|
|||||||
/// <summary>Reads a typed value from the node data.</summary>
|
/// <summary>Reads a typed value from the node data.</summary>
|
||||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||||
|
|
||||||
|
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
|
||||||
|
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> _battlePass.GetProgressAsync(Context.ScenarioId, Context.NodeId, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>Credits xp from a configured source.</summary>
|
||||||
|
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
|
||||||
|
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
|
||||||
|
{
|
||||||
|
ScenarioId = Context.ScenarioId,
|
||||||
|
NodeId = Context.NodeId,
|
||||||
|
RunId = Context.RunId,
|
||||||
|
Source = source,
|
||||||
|
Amount = amount
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Claims a tier reward at a reached level.
|
||||||
|
/// <paramref name="track"/> is <see cref="BattlePassService.TrackFree"/> or <see cref="BattlePassService.TrackPremium"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||||
|
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
|
||||||
|
{
|
||||||
|
ScenarioId = Context.ScenarioId,
|
||||||
|
NodeId = Context.NodeId,
|
||||||
|
RunId = Context.RunId,
|
||||||
|
Level = level,
|
||||||
|
Track = track
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>Purchases the premium track, then crosses onPremiumPurchase on success.</summary>
|
||||||
|
public async Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||||
|
{
|
||||||
|
ScenarioId = Context.ScenarioId,
|
||||||
|
NodeId = Context.NodeId,
|
||||||
|
RunId = Context.RunId,
|
||||||
|
IdempotencyKey = Guid.NewGuid().ToString()
|
||||||
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response != null && response.Success)
|
||||||
|
await Context.CompleteAsync("onPremiumPurchase", cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return response!;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Advances the run through the onLevelUp handle.</summary>
|
/// <summary>Advances the run through the onLevelUp handle.</summary>
|
||||||
public Task LevelUpAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onLevelUp", cancellationToken);
|
public Task LevelUpAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onLevelUp", cancellationToken);
|
||||||
|
|
||||||
@@ -29,12 +87,6 @@ public sealed class BattlePassSession
|
|||||||
/// <summary>Advances the run through the onMaxLevel handle (fire-and-forget).</summary>
|
/// <summary>Advances the run through the onMaxLevel handle (fire-and-forget).</summary>
|
||||||
public void MaxLevel() => Context.Complete("onMaxLevel");
|
public void MaxLevel() => Context.Complete("onMaxLevel");
|
||||||
|
|
||||||
/// <summary>Advances the run through the onPremiumPurchase handle.</summary>
|
|
||||||
public Task PremiumPurchaseAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onPremiumPurchase", cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>Advances the run through the onPremiumPurchase handle (fire-and-forget).</summary>
|
|
||||||
public void PremiumPurchase() => Context.Complete("onPremiumPurchase");
|
|
||||||
|
|
||||||
/// <summary>Advances the run through the onComplete handle.</summary>
|
/// <summary>Advances the run through the onComplete handle.</summary>
|
||||||
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core;
|
|
||||||
|
|
||||||
/// <summary>User-generated content: upload, list, download URLs, deletion.</summary>
|
|
||||||
public sealed class UgcService
|
|
||||||
{
|
|
||||||
private readonly RudderClient _client;
|
|
||||||
|
|
||||||
internal UgcService(RudderClient client) => _client = client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uploads a file in one call: pre-signed URL, PUT of the bytes, submission.
|
|
||||||
/// Requires an upload transport in <see cref="RudderClientOptions.UploadTransport"/>.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<UgcSubmission> UploadAsync(
|
|
||||||
string name,
|
|
||||||
byte[] data,
|
|
||||||
string? description = null,
|
|
||||||
JToken? metadata = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var uploadTransport = _client.Options.UploadTransport
|
|
||||||
?? throw new InvalidOperationException("UploadTransport is not configured.");
|
|
||||||
|
|
||||||
var upload = await GetUploadUrlAsync(name, cancellationToken).ConfigureAwait(false);
|
|
||||||
await uploadTransport.PutAsync(
|
|
||||||
upload.UploadUrl,
|
|
||||||
data ?? Array.Empty<byte>(),
|
|
||||||
GetContentType(name),
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return await SubmitAsync(
|
|
||||||
name,
|
|
||||||
upload.FileKey,
|
|
||||||
data?.LongLength ?? 0,
|
|
||||||
description,
|
|
||||||
metadata,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fetches one page of the player's submissions.</summary>
|
|
||||||
public Task<ListUgcResponse> ListAsync(string? status = null, int limit = 20, string? cursor = null, CancellationToken cancellationToken = default)
|
|
||||||
=> _client.SendAsync<ListUgcResponse>(
|
|
||||||
"GET",
|
|
||||||
"/sdk/v1/ugc" + Url.Query(
|
|
||||||
("status", status),
|
|
||||||
("limit", limit > 0 ? limit.ToString() : null),
|
|
||||||
("cursor", cursor)),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>Iterates over all of the player's submissions, following the cursor pagination.</summary>
|
|
||||||
public async IAsyncEnumerable<UgcSubmission> ListAllAsync(
|
|
||||||
string? status = null,
|
|
||||||
int limit = 20,
|
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
string? cursor = null;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var page = await ListAsync(status, limit, cursor, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (page?.Items != null)
|
|
||||||
{
|
|
||||||
foreach (var item in page.Items)
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = page?.NextCursor;
|
|
||||||
}
|
|
||||||
while (!string.IsNullOrEmpty(cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Returns one submission.</summary>
|
|
||||||
public Task<UgcSubmission> GetAsync(string id, CancellationToken cancellationToken = default)
|
|
||||||
=> _client.SendAsync<UgcSubmission>("GET", "/sdk/v1/ugc/" + Url.Encode(id), cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>Deletes a submission; returns false when the server refused.</summary>
|
|
||||||
public async Task<bool> DeleteAsync(string id, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var response = await _client.SendAsync<DeleteUgcResponse>(
|
|
||||||
"DELETE",
|
|
||||||
"/sdk/v1/ugc/" + Url.Encode(id),
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return response == null || response.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Requests a pre-signed upload URL.</summary>
|
|
||||||
public Task<GetUploadUrlResponse> GetUploadUrlAsync(string filename, CancellationToken cancellationToken = default)
|
|
||||||
=> _client.SendAsync<GetUploadUrlResponse>(
|
|
||||||
"GET",
|
|
||||||
"/sdk/v1/ugc/upload-url" + Url.Query(("filename", filename)),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the download URL of a submission.
|
|
||||||
/// Throws <see cref="InvalidOperationException"/> when the server returned none.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<string> GetDownloadUrlAsync(string id, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var response = await _client.SendAsync<GetDownloadUrlResponse>(
|
|
||||||
"GET",
|
|
||||||
"/sdk/v1/ugc/" + Url.Encode(id) + "/download",
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (response == null || string.IsNullOrEmpty(response.DownloadUrl))
|
|
||||||
throw new InvalidOperationException("The server returned no download URL.");
|
|
||||||
|
|
||||||
return response.DownloadUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Registers an uploaded file as a submission.</summary>
|
|
||||||
public Task<UgcSubmission> SubmitAsync(
|
|
||||||
string name,
|
|
||||||
string fileKey,
|
|
||||||
long fileSize,
|
|
||||||
string? description = null,
|
|
||||||
JToken? metadata = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return _client.SendAsync<SubmitUgcRequest, UgcSubmission>(
|
|
||||||
"POST",
|
|
||||||
"/sdk/v1/ugc",
|
|
||||||
new SubmitUgcRequest
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
FileKey = fileKey,
|
|
||||||
FileSize = fileSize,
|
|
||||||
Description = description!,
|
|
||||||
Metadata = metadata!
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetContentType(string filename)
|
|
||||||
{
|
|
||||||
var ext = System.IO.Path.GetExtension(filename)?.ToLowerInvariant();
|
|
||||||
switch (ext)
|
|
||||||
{
|
|
||||||
case ".png": return "image/png";
|
|
||||||
case ".jpg":
|
|
||||||
case ".jpeg": return "image/jpeg";
|
|
||||||
case ".gif": return "image/gif";
|
|
||||||
case ".webp": return "image/webp";
|
|
||||||
case ".txt": return "text/plain";
|
|
||||||
default: return "application/octet-stream";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class DeleteUgcResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("success")]
|
|
||||||
public bool Success { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class GetDownloadUrlResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("downloadUrl")]
|
|
||||||
public string DownloadUrl { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class GetUploadUrlResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("fileKey")]
|
|
||||||
public string FileKey { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("uploadUrl")]
|
|
||||||
public string UploadUrl { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class ListUgcResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("items")]
|
|
||||||
public List<UgcSubmission> Items { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("nextCursor")]
|
|
||||||
public string NextCursor { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class SubmitUgcRequest
|
|
||||||
{
|
|
||||||
[JsonProperty("description")]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("fileKey")]
|
|
||||||
public string FileKey { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("fileSize")]
|
|
||||||
public long FileSize { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("metadata")]
|
|
||||||
public JToken Metadata { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class SubmittedBy
|
|
||||||
{
|
|
||||||
[JsonProperty("userId")]
|
|
||||||
public string UserId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("username")]
|
|
||||||
public string Username { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Code generated by apigen. DO NOT EDIT.
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RudderSdk.Core.Models.Ugc;
|
|
||||||
|
|
||||||
public class UgcSubmission
|
|
||||||
{
|
|
||||||
[JsonProperty("description")]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("fileSize")]
|
|
||||||
public long FileSize { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("fileUrl")]
|
|
||||||
public string FileUrl { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("id")]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("metadata")]
|
|
||||||
public JToken Metadata { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("reviewedAt")]
|
|
||||||
public string ReviewedAt { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("reviewedBy")]
|
|
||||||
public string ReviewedBy { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("status")]
|
|
||||||
public string Status { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("submittedAt")]
|
|
||||||
public string SubmittedAt { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("submittedBy")]
|
|
||||||
public SubmittedBy SubmittedBy { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,6 @@ public sealed class RudderClientTests
|
|||||||
Assert.NotNull(client.Inventory);
|
Assert.NotNull(client.Inventory);
|
||||||
Assert.NotNull(client.BattlePass);
|
Assert.NotNull(client.BattlePass);
|
||||||
Assert.NotNull(client.Quests);
|
Assert.NotNull(client.Quests);
|
||||||
Assert.NotNull(client.Ugc);
|
|
||||||
Assert.NotNull(client.Scenario);
|
Assert.NotNull(client.Scenario);
|
||||||
Assert.NotNull(client.Realtime);
|
Assert.NotNull(client.Realtime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ public sealed class ScenarioServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Quest_Progress_Response_Can_Start_Continuation_Plan()
|
public async Task Quest_Progress_Completed_Response_Completes_The_Node()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
transport.Enqueue(new TriggerScenarioResponse
|
transport.Enqueue(new TriggerScenarioResponse
|
||||||
@@ -275,16 +275,12 @@ public sealed class ScenarioServiceTests
|
|||||||
Plan("plan", Node("quest", "quest"))
|
Plan("plan", Node("quest", "quest"))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
transport.Enqueue(new UpdateScenarioCounterResponse
|
transport.Enqueue(new UpdateScenarioCounterResponse { Completed = true });
|
||||||
{
|
|
||||||
Completed = true,
|
|
||||||
Plan = Plan("continuation", Node("done", "notification"))
|
|
||||||
});
|
|
||||||
var client = CreateClient(transport);
|
var client = CreateClient(transport);
|
||||||
QuestSession? quest = null;
|
QuestSession? quest = null;
|
||||||
var notifications = 0;
|
var completed = 0;
|
||||||
client.Scenario.OnQuest += session => quest = session;
|
client.Scenario.OnQuest += session => quest = session;
|
||||||
client.Scenario.OnNotification += _ => notifications++;
|
client.Scenario.OnScenarioCompleted += _ => completed++;
|
||||||
|
|
||||||
await client.Scenario.TriggerAsync("login");
|
await client.Scenario.TriggerAsync("login");
|
||||||
await quest!.AddProgressAsync("wins", 1);
|
await quest!.AddProgressAsync("wins", 1);
|
||||||
@@ -293,11 +289,40 @@ public sealed class ScenarioServiceTests
|
|||||||
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/counter").Request);
|
transport.Calls.Single(call => call.Path == "/sdk/v1/scenarios/counter").Request);
|
||||||
Assert.Equal("quest", counter.NodeId);
|
Assert.Equal("quest", counter.NodeId);
|
||||||
Assert.Equal("wins", counter.CounterKey);
|
Assert.Equal("wins", counter.CounterKey);
|
||||||
Assert.Equal(1, notifications);
|
Assert.Equal(1, completed);
|
||||||
|
Assert.Empty(client.Scenario.ActiveRuns);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Unknown_Nodes_Are_Logged_And_Ignored()
|
public async Task Quest_Progress_Failure_Does_Not_Fail_The_Run()
|
||||||
|
{
|
||||||
|
var transport = new FakeTransport();
|
||||||
|
transport.Enqueue(new TriggerScenarioResponse
|
||||||
|
{
|
||||||
|
Plans = new List<ExecutionPlan>
|
||||||
|
{
|
||||||
|
Plan("plan", Node("quest", "quest"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
transport.Enqueue(new InvalidOperationException("boom"));
|
||||||
|
var logger = new FakeLogger();
|
||||||
|
var client = CreateClient(transport, logger: logger);
|
||||||
|
QuestSession? quest = null;
|
||||||
|
var failed = 0;
|
||||||
|
client.Scenario.OnQuest += session => quest = session;
|
||||||
|
client.Scenario.OnScenarioFailed += _ => failed++;
|
||||||
|
|
||||||
|
await client.Scenario.TriggerAsync("login");
|
||||||
|
await quest!.AddProgressAsync("wins", 1);
|
||||||
|
|
||||||
|
Assert.Equal(0, failed);
|
||||||
|
Assert.Single(client.Scenario.ActiveRuns);
|
||||||
|
Assert.Contains(logger.Messages, m =>
|
||||||
|
m.Level == RudderLogLevel.Warning && m.Message.Contains("counter update failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Unknown_Nodes_Fail_The_Run()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
transport.Enqueue(new TriggerScenarioResponse
|
transport.Enqueue(new TriggerScenarioResponse
|
||||||
@@ -309,12 +334,16 @@ public sealed class ScenarioServiceTests
|
|||||||
});
|
});
|
||||||
var logger = new FakeLogger();
|
var logger = new FakeLogger();
|
||||||
var client = CreateClient(transport, logger: logger);
|
var client = CreateClient(transport, logger: logger);
|
||||||
|
ScenarioFailedEvent? failure = null;
|
||||||
|
client.Scenario.OnScenarioFailed += e => failure = e;
|
||||||
|
|
||||||
await client.Scenario.TriggerAsync("login");
|
await client.Scenario.TriggerAsync("login");
|
||||||
|
|
||||||
var (level, message) = Assert.Single(logger.Messages);
|
Assert.NotNull(failure);
|
||||||
Assert.Equal(RudderLogLevel.Warning, level);
|
Assert.Equal("future", failure!.NodeId);
|
||||||
Assert.Contains("Unsupported scenario node type 'future_node'", message);
|
Assert.Empty(client.Scenario.ActiveRuns);
|
||||||
|
Assert.Contains(logger.Messages, m =>
|
||||||
|
m.Level == RudderLogLevel.Warning && m.Message.Contains("Unsupported scenario node type 'future_node'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RudderClient CreateClient(
|
private static RudderClient CreateClient(
|
||||||
|
|||||||
Reference in New Issue
Block a user