Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Tracking issue

<!-- Link the relevant lootlocker/index issue. Use "Closes lootlocker/index#<number>" or a plain link. -->

## Description

<!-- Short summary of what changed and why. -->

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Refactor
- [ ] Docs
- [ ] Chore

## Checklist

- [ ] PR targets `dev` branch (not `main`)
- [ ] Branch follows naming convention (`feat/`, `fix/`, `docs/`, `refactor/`, `chore/`, etc.)
- [ ] Diff is minimal and scoped — no unrelated changes, no drive-by refactors
- [ ] No version bumps or release metadata changes (unless explicitly requested)
- [ ] No new helpers/utilities introduced without first searching for existing ones
- [ ] Compile Check CI workflow passes (or verification has been run locally)
- [ ] Runtime code has no unguarded `UnityEditor` dependencies
- [ ] Editor-only code is placed under `Runtime/Editor/` and/or guarded with `#if UNITY_EDITOR`
- [ ] Tracking issue in `lootlocker/index` is linked and status set to "In Review"

## Testing notes

<!-- Describe how the change was verified (manual steps, automated tests, etc.). -->

## Additional notes

<!-- Anything else the reviewer should know (optional). -->
45 changes: 45 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copilot / Coding Agent instructions (LootLocker Unity SDK)

Follow these rules for any work in this repo:

## Non-negotiables
- Never commit directly to `dev` or `main`.
- PRs must target `dev`.
- Do not tag/publish/create releases.
- Do not bump versions or edit release metadata (for example `package.json` version) unless explicitly asked.
- Keep diffs minimal; do not move/rename files unless explicitly requested.
- Search first to avoid duplicating helpers/utilities.

## Architecture references
- Repo structure + “where do I implement X?”: `.github/instructions/architecture.md`
- Guardrails (agent operating rules): `.github/instructions/guardrails.md`

## Verification (compile & test before PR)
- How to verify changes (local + CI): `.github/instructions/verification.md`
- Cloud agent: push to work branch → wait for **Compile Check** workflow.
- Local: run `.github/scripts/verify-compilation.sh` (Linux/macOS) or `.github\scripts\verify-compilation.ps1` (Windows) after creating `unity-dev-settings.json` from the example:
```json
{
"unity_executable": "<absolute path to Unity binary>",
"test_project_path": ""
}
```

## Conventions & style
- Coding conventions & style guide: `.github/instructions/style-guide.md`
- Patterns cookbook (templates): `.github/instructions/patterns.md`
- Path-specific instructions:
- Public API surface (`Runtime/Game/LootLockerSDKManager.cs`): `.github/instructions/Runtime/Game/LootLockerSDKManager.cs.instructions.md`
- Request implementations (`Runtime/Game/Requests/**`): `.github/instructions/Runtime/Game/Requests.instructions.md`
- PlayMode tests (`Tests/LootLockerTests/PlayMode/**`): `.github/instructions/Tests/LootLockerTests/PlayMode.instructions.md`
- Test utilities (`Tests/LootLockerTestUtils/**`): `.github/instructions/Tests/LootLockerTestUtils.instructions.md`

## Testing
- How to write and run tests: `.github/instructions/testing.md`
- Local: `.github\scripts\run-tests.ps1 -TestCategory LootLockerCIFast`
- Cloud agent: **Actions → Run Tests → Run workflow** (supports `testCategory` and `testFilter` inputs).
- How to use tests for debugging (temporary debug tests): `.github/instructions/debugging.md`
- Use `Category("LootLockerDebug")` for temporary debug tests; **always delete before committing**.

## Issue Tracking & Lifecycle
- Full lifecycle rules (status updates, PR linking, DoD): `.github/instructions/implementation-lifecycle.md`
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
applyTo: "Runtime/Game/LootLockerSDKManager.cs"
---

# Scoped instructions: `LootLockerSDKManager.cs` (public API surface)

- Treat this file as the primary, customer-facing API surface. Preserve backward compatibility.
- Prefer additive changes (new methods/overloads) over breaking changes to existing signatures, default values, namespaces, or behavior.
- Keep API methods thin: route work to existing request implementations under `Runtime/Game/Requests/` (or shared helpers) rather than duplicating endpoint/transport logic here.
- Always place methods in this file in a `#region` block corresponding to their feature set (for example, Authentication, Inventory, etc.) and keep them organized with related methods.
- XML docs are required for any `public` API you add or change. Match the existing doc style in this file, including clear `param` descriptions and any practical usage notes.

## XML doc template (adjust as needed)

```csharp
/// <summary>
/// One-sentence description of what the method does.
///
/// Optional additional details or usage notes.
/// </summary>
/// <param name="forPlayerWithUlid"> Optional : Execute the request for the specified player. If not supplied, the default player will be used. </param>
/// <param name="onComplete"> onComplete Action for handling the response </param>
/// <returns>
/// Return value semantics if this API returns a value. Otherwise omit this tag.
/// </returns>
public static void ExampleMethod(string forPlayerWithUlid, Action<LootLockerExampleResponse> onComplete)
{
// Keep facade thin; validate input early; call onComplete once on all paths.
}
```
- If a method takes a player selector, follow the existing `forPlayerWithUlid` convention (optional when appropriate; don’t invent new parameter names for the same concept).
- For callback-based APIs, call `onComplete` exactly once on all code paths (including validation failures).
- Do not log secrets or raw tokens from this layer. Use `LootLockerLogger` (not `Debug.Log`) and rely on log obfuscation (`LootLockerConfig.current.obfuscateLogs`).
- Use the repo’s JSON wrapper (`LootLockerJson`) and existing response/error helpers; do not introduce new serialization/logging dependencies.
- Keep diffs minimal and localized; avoid unrelated refactors in this large file.
18 changes: 18 additions & 0 deletions .github/instructions/Runtime/Game/Requests.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
applyTo: "Runtime/Game/Requests/**/*.cs"
---

# Scoped instructions: `Runtime/Game/Requests/` (request implementations)

- Follow the established split:
- DTOs/response models in `namespace LootLocker.Requests`.
- Request methods in `namespace LootLocker` inside `public partial class LootLockerAPIManager`.
- Responses should inherit `LootLockerResponse` and use property names that match server JSON (commonly `snake_case`), as seen throughout this folder.
- Validate inputs early. For an unserializable/missing body, return a consistent error response via `LootLockerResponseFactory` and invoke `onComplete` once.
- Serialize request bodies with `LootLockerJson.SerializeObject(...)` (never call Newtonsoft/ZeroDep JSON APIs directly).
- Build endpoints via `LootLockerEndPoints` + `EndPointClass.WithPathParameter(s)`; avoid manual string concatenation for path params.
- For query parameters, prefer `LootLocker.Utilities.HTTP.QueryParamaterBuilder` when available; otherwise ensure values are URL-encoded (for example `WebUtility.UrlEncode`).
- Use the standard transport pattern:
- `LootLockerServerRequest.CallAPI(..., onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }, useAuthToken: true/false)`
- Keep `useAuthToken` consistent with the endpoint’s auth requirements and nearby examples. This variable is almost always `true`.
- Keep methods small and consistent with surrounding files; don’t move DTOs or rename existing public types unless explicitly requested.
69 changes: 69 additions & 0 deletions .github/instructions/Tests/LootLockerTestUtils.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
applyTo: "Tests/LootLockerTestUtils/**/*.cs"
---

# Scoped instructions: `Tests/LootLockerTestUtils/` (test configuration utilities)

This folder holds the admin-API client and helpers that tests use to provision and tear down
real LootLocker game configurations. It is **not** test code itself — it is infrastructure
that backs the tests under `Tests/LootLockerTests/PlayMode/`.

## Key classes

| Class / File | Responsibility |
|---|---|
| `LootLockerTestGame` | Creates/deletes a game, exposes helpers for common game setup (enable platforms, create leaderboards, etc.). |
| `LootLockerTestUser` | Gets or signs up a time-scoped admin user so the admin API calls are authenticated. |
| `Auth` | Low-level admin login / signup via the LootLocker admin API. |
| `LootLockerAdminRequest` | Thin wrapper around `LootLockerServerRequest.CallAPI` that injects the `Admin` caller role and handles rate-limit retries. |
| `LootLockerTestConfigurationEndpoints` | Constants for all admin API endpoints used by the utilities. |

## How authentication works

`LootLockerTestUser.GetUserOrSignIn` auto-derives a deterministic email from the current
date (e.g. `unity+ci-testrun+2024-04-17-14h@lootlocker.com`) and the same string as the
password, then attempts login and falls back to signup on a 401. This means test runs
are self-contained and need no pre-provisioned secrets in stage environments.

## Transport pattern

All admin API calls use `LootLockerAdminRequest.Send(...)`, which sets
`callerRole: LootLockerCallerRole.Admin`. Never call `LootLockerServerRequest.CallAPI`
directly from this folder — go through `LootLockerAdminRequest.Send`.

```csharp
LootLockerAdminRequest.Send(endPoint.endPoint, endPoint.httpMethod, json,
onComplete: (serverResponse) =>
{
var response = LootLockerResponse.Deserialize<MyResponse>(serverResponse);
onComplete?.Invoke(response);
}, useAuthToken: true);
```

## Adding a new admin API helper

1. Add the endpoint constant to `LootLockerTestConfigurationEndpoints.cs`.
2. Create a request/response DTO (plain class or `LootLockerResponse` subclass) in the
relevant file, or add a new file for a new domain area.
3. Add a static method to the appropriate class (`LootLockerTestGame`, `LootLockerTestAssets`,
etc.) that:
- Checks `if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken))` first and
invokes `onComplete` with a failed response if not authenticated.
- Uses `LootLockerAdminRequest.Send(...)` for transport.
- Deserializes with `LootLockerResponse.Deserialize<T>(serverResponse)`.
4. Map the requests to the correct endpoint according to the api docs: https://ref.lootlocker.com/admin

## Namespace

All classes in this folder live in `namespace LootLockerTestConfigurationUtils`.

## Conventions

- Keep methods callback-based (`Action<...> onComplete`) to match the async Unity pattern
used by tests (`yield return new WaitUntil(() => done)`).
- Do not reference `LootLockerTests` from this utility layer — the dependency runs one way:
tests depend on utils, not the other way.
- Use `LootLockerJson.SerializeObject(...)` for request body serialization — same as runtime.
- Error paths must always invoke `onComplete` exactly once.
- Endpoint strings that include a game ID placeholder use `#GAMEID#`, which
`LootLockerAdminRequest` replaces automatically from `LootLockerAdminRequest.ActiveGameId`.
191 changes: 191 additions & 0 deletions .github/instructions/Tests/LootLockerTests/PlayMode.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
---
applyTo: "Tests/LootLockerTests/PlayMode/**/*.cs"
---

# Scoped instructions: `Tests/LootLockerTests/PlayMode/` (PlayMode integration tests)

These are the SDK's integration tests. They run as Unity PlayMode tests and use the
`LootLockerTestUtils` infrastructure to create and tear down a real, isolated LootLocker
game via the admin API for every test run.

## Anatomy of a test file

Every production test file follows this structure exactly:

```csharp
using System.Collections;
using LootLocker;
using LootLocker.Requests;
using LootLockerTestConfigurationUtils;
using NUnit.Framework;
using UnityEditor; // only if required by the feature under test
using UnityEngine;
using UnityEngine.TestTools;

namespace LootLockerTests.PlayMode
{
public class MyFeatureTest
{
private LootLockerTestGame gameUnderTest = null;
private LootLockerConfig configCopy = null;
private static int TestCounter = 0;
private bool SetupFailed = false;

[UnitySetUp]
public IEnumerator Setup()
{
TestCounter++;
configCopy = LootLockerConfig.current;
Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} setup #####");

if (!LootLockerConfig.ClearSettings())
{
Debug.LogError("Could not clear LootLocker config");
}

// Create isolated game
bool gameCreationCallCompleted = false;
LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) =>
{
if (!success)
{
SetupFailed = true;
gameCreationCallCompleted = true;
return;
}
gameUnderTest = game;
gameCreationCallCompleted = true;
});
yield return new WaitUntil(() => gameCreationCallCompleted);
if (SetupFailed) { yield break; }

gameUnderTest?.SwitchToStageEnvironment();

// Enable required platform(s) — add only what the feature under test needs
bool enableGuestLoginCallCompleted = false;
gameUnderTest?.EnableGuestLogin((success, errorMessage) =>
{
if (!success) { SetupFailed = true; }
enableGuestLoginCallCompleted = true;
});
yield return new WaitUntil(() => enableGuestLoginCallCompleted);
if (SetupFailed) { yield break; }

Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Failed to initialize LootLocker");

// Start a default session for the test
bool sessionCompleted = false;
LootLockerSDKManager.StartGuestSession(GUID.Generate().ToString(), response =>
{
SetupFailed |= !response.success;
sessionCompleted = true;
});
yield return new WaitUntil(() => sessionCompleted);

Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####");
}

[UnityTearDown]
public IEnumerator TearDown()
{
Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} test case #####");
if (gameUnderTest != null)
{
bool gameDeletionCallCompleted = false;
gameUnderTest.DeleteGame(((success, errorMessage) =>
{
if (!success) { Debug.LogError(errorMessage); }
gameUnderTest = null;
gameDeletionCallCompleted = true;
}));
yield return new WaitUntil(() => gameDeletionCallCompleted);
}
LootLockerStateData.ClearAllSavedStates();
LootLockerConfig.CreateNewSettings(configCopy);
Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####");
}

[UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")]
public IEnumerator MyFeature_DoesExpectedThing_Succeeds()
{
Assert.IsFalse(SetupFailed, "Failed to setup game");

// Given
// ...

// When
MyResponseType actualResponse = null;
bool callCompleted = false;
LootLockerSDKManager.SomeMethod(param, response =>
{
actualResponse = response;
callCompleted = true;
});
yield return new WaitUntil(() => callCompleted);

// Then
Assert.IsTrue(actualResponse.success, "Expected call to succeed");
Assert.AreEqual(expected, actualResponse.someField, "Got unexpected value");
}
}
}
```

## Test method naming

Format: `<Feature>_<Action>_<ExpectedOutcome>`

Examples:
- `Leaderboard_ListTopTen_ReturnsScoresInDescendingOrder`
- `GuestSession_StartWithValidId_Succeeds`
- `PlayerStorage_UpdateNonExistentKey_CreatesIt`

## Category attributes

Every production test method must carry the appropriate category tags:

| Category | Meaning |
|---|---|
| `LootLocker` | All SDK tests — the catch-all for manual / local runs. |
| `LootLockerCI` | Included in the standard CI run (full suite). |
| `LootLockerCIFast` | Included in the fast CI subset. **Only add this if the test consistently finishes in under ~10 seconds.** |

Typical annotation: `[UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")]`

Do **not** use `Category("LootLockerDebug")` on production tests — that category is reserved
for temporary debugging tests that must be removed before any PR.

## Game lifecycle rules

Every test class creates a **fresh isolated game** per test run; there is no shared state
between test methods. This keeps tests hermetic but requires strict discipline:

- Always call `gameUnderTest.DeleteGame(...)` in `TearDown` even if `SetupFailed` — otherwise
orphaned games accumulate in the admin account.
- Always guard teardown with `if (gameUnderTest != null)`.
- Always check `if (SetupFailed) { yield break; }` after every blocking setup step.
- Call `LootLockerStateData.ClearAllSavedStates()` and `LootLockerConfig.CreateNewSettings(configCopy)`
at the end of every `TearDown` to restore SDK state for the next test.

## Test body conventions

- First line of every test: `Assert.IsFalse(SetupFailed, "Failed to setup game");`
- Follow **Given / When / Then** comment structure inside the test body.
- Use `yield return new WaitUntil(() => done)` to await async SDK calls; always
pair with a `bool done = false` flag set in the callback.
- Use NUnit `Assert.*`; never use `Debug.LogError` as a substitute for assertions.
- Do not add `using UnityEditor` unless the test must run in edit mode.

## One test class per test file

One file = one public class. Keep feature scope narrow — a file named `LeaderboardTest.cs`
should only cover the Leaderboard feature.

## What NOT to do

- Do not leave debugging logic (`Debug.Log` spam, disabled asserts as comments, etc.) in
committed code.
- Do not skip the `TearDown` game deletion — orphaned admin games are costly.
- Do not commit any test with `Category("LootLockerDebug")` — see `.github/instructions/debugging.md`.
- Do not add new test helpers or shared utilities directly in this folder; put them under
`Tests/LootLockerTestUtils/` instead (see `.github/instructions/Tests/LootLockerTestUtils.instructions.md`).
Loading
Loading