Skip to content

Commit f6a7351

Browse files
committed
Add executor API support and test project
Introduces support for executor-related endpoints, including models for Executor and ExecutorInfo, and updates the Executors API to fetch versions, executor lists, and executor info. Adds error deserialization and improved error handling in Client. Adds a new ScriptBloxAPI.Tests project with tests for scripts, comments, executors, and user stats. Updates solution and project files to include the new test project and exclude test files from the main build.
1 parent 17c5d1a commit f6a7351

File tree

15 files changed

+277
-33
lines changed

15 files changed

+277
-33
lines changed

Client.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq;
43
using System.Net.Http;
54
using System.Text.Json;
65
using System.Threading.Tasks;
6+
using ScriptBloxApi.Objects;
77

88
namespace ScriptBloxApi
99
{
@@ -22,6 +22,7 @@ internal class Client
2222

2323
internal static HttpClient HttpClient => LazyClient.Value;
2424

25+
#nullable enable
2526
internal static async Task<T> Get<T>(string endpoint, (string Key, string Value)[]? queryParams)
2627
{
2728
string queryString = string.Join("&", queryParams?.Select(kvp => $"{kvp.Key}={kvp.Value}") ?? []);
@@ -33,15 +34,41 @@ internal static async Task<T> Get<T>(string endpoint, (string Key, string Value)
3334
HttpResponseMessage response = await HttpClient.SendAsync(request);
3435

3536
if (!response.IsSuccessStatusCode)
36-
throw new Exception(
37-
$"Error fetching: {response.StatusCode}\n{await response.Content.ReadAsStringAsync()}");
37+
{
38+
string responseText = await response.Content.ReadAsStringAsync();
39+
(bool success, Error? data) = TryDeserialize<Error>(responseText);
40+
41+
if (success && data is not null)
42+
throw new Exception(data.Message);
43+
44+
throw new Exception($"Error fetching: {response.StatusCode}\n{await response.Content.ReadAsStringAsync()}");
45+
}
46+
3847

3948
string jsonResponse = await response.Content.ReadAsStringAsync();
4049

4150
if (typeof(T) == typeof(string))
4251
return (T)(object)jsonResponse;
4352

44-
return JsonSerializer.Deserialize<T>(jsonResponse);
53+
T? result = JsonSerializer.Deserialize<T>(jsonResponse);
54+
55+
if (result is null)
56+
throw new Exception("Deserialization returned null.");
57+
58+
return result;
59+
}
60+
61+
internal static (bool, T?) TryDeserialize<T>(string jsonResponse)
62+
{
63+
try
64+
{
65+
return (true, JsonSerializer.Deserialize<T>(jsonResponse));
66+
}
67+
catch
68+
{
69+
return (false, default);
70+
}
4571
}
72+
#nullable disable
4673
}
4774
}

Executors/Executors.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
1+
using System.Collections.Generic;
42
using System.Threading.Tasks;
3+
using ScriptBloxApi.Objects;
54

65
namespace ScriptBloxApi.Executors
76
{
87
public static class Executors
98
{
10-
public static async Task<IReadOnlyList<Version>> GetRobloxVersions()
11-
{
12-
IReadOnlyList<Version> result = await Client.Get<IReadOnlyList<Version>>("roblox/versions", []);
13-
return result;
14-
}
9+
public static async Task<IReadOnlyList<Versions>> GetRobloxVersions() => await Client.Get<IReadOnlyList<Versions>>("roblox/versions", []);
1510

16-
public static async Task<string> GetExecutorList()
17-
{
18-
//await Client.Get<IReadOnlyList<Version>>("executor/list", []); They haven't finished it on their end
19-
return "";
20-
}
11+
public static async Task<IReadOnlyList<Executor>> GetExecutorList() => await Client.Get<IReadOnlyList<Executor>>("executor/list", []);
12+
13+
public static async Task<ExecutorInfo> GetExecutorInfo(string executor) => await Client.Get<ExecutorInfo>($"executor/{executor}", []);
2114
}
2215
}

LanguageFeatures/IntModification.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
4-
5-
namespace ScriptBloxApi.LanguageFeatures
1+
namespace ScriptBloxApi.LanguageFeatures
62
{
73
internal static class IntModification
84
{

Objects/Error.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ScriptBloxApi.Objects;
4+
5+
public record Error(
6+
[property: JsonPropertyName("message")] string Message
7+
);

Objects/Executors.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json.Serialization;
4+
5+
namespace ScriptBloxApi.Objects;
6+
7+
public record Executor(
8+
[property: JsonPropertyName("_id")] string Id,
9+
[property: JsonPropertyName("name")] string Name,
10+
[property: JsonPropertyName("patched")] bool? Patched,
11+
[property: JsonPropertyName("platform")] string Platform,
12+
[property: JsonPropertyName("website")] string Website,
13+
[property: JsonPropertyName("discord")] string Discord,
14+
[property: JsonPropertyName("version")] string Version,
15+
[property: JsonPropertyName("versionDate")] DateTime? VersionDate,
16+
[property: JsonPropertyName("thumbnail")] string Thumbnail,
17+
[property: JsonPropertyName("store")] string Store,
18+
[property: JsonPropertyName("type")] string Type
19+
);
20+
21+
public record ExecutorInfo(
22+
[property: JsonPropertyName("_id")] string Id,
23+
[property: JsonPropertyName("name")] string Name,
24+
[property: JsonPropertyName("description")] string Description,
25+
[property: JsonPropertyName("website")] string Website,
26+
[property: JsonPropertyName("discord")] string Discord,
27+
[property: JsonPropertyName("developers")] string Developers,
28+
[property: JsonPropertyName("images")] IReadOnlyList<string> Images,
29+
[property: JsonPropertyName("showcase")] string Showcase,
30+
[property: JsonPropertyName("thumbnail")] string Thumbnail,
31+
[property: JsonPropertyName("store")] string Store
32+
);

Objects/UserFollowing.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
1+
using System.Collections.Generic;
42
using System.Text.Json.Serialization;
53

64
namespace ScriptBloxApi.Objects;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using ScriptBloxApi.Objects;
2+
using ScriptBloxApi.Scripts;
3+
4+
namespace ScriptBloxAPI.Tests
5+
{
6+
public class CommentsTests
7+
{
8+
[Fact]
9+
public async Task GetScriptCommentsAsync_ReturnsList()
10+
{
11+
IReadOnlyList<Comment>? comments = await Comments.GetScriptCommentsAsync("61abb678404510c9285337ec"); // https://scriptblox.com/script/GUI_57
12+
Assert.NotNull(comments);
13+
Assert.All(comments, c => Assert.IsType<Comment>(c));
14+
}
15+
}
16+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using ScriptBloxApi.Executors;
2+
using ScriptBloxApi.Objects;
3+
4+
namespace ScriptBloxAPI.Tests
5+
{
6+
public class ExecutorsTests
7+
{
8+
[Fact]
9+
public async Task GetRobloxVersions_ReturnsList()
10+
{
11+
IReadOnlyList<Versions>? versions = await Executors.GetRobloxVersions();
12+
Assert.NotEmpty(versions);
13+
}
14+
15+
[Fact]
16+
public async Task GetExecutorList_ReturnsExecutors()
17+
{
18+
IReadOnlyList<Executor>? list = await Executors.GetExecutorList();
19+
Assert.NotEmpty(list);
20+
}
21+
22+
[Fact]
23+
public async Task GetExecutorInfo_ReturnsExecutorInfo()
24+
{
25+
ExecutorInfo? info = await Executors.GetExecutorInfo("Cryptic"); // replace with known valid executor
26+
Assert.NotNull(info);
27+
Assert.IsType<ExecutorInfo>(info);
28+
}
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
15+
<PackageReference Include="xunit" Version="2.5.3" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\ScriptBloxApi.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Using Include="Xunit" />
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using ScriptBloxApi.Objects;
2+
using ScriptBloxApi.Scripts;
3+
4+
namespace ScriptBloxAPI.Tests
5+
{
6+
public class ScriptsTests
7+
{
8+
9+
[Theory]
10+
// Basic default call
11+
[InlineData(1, 10, null, null, null, null, null, null, null)]
12+
13+
// Free scripts, sorted by views descending
14+
[InlineData(1, 5, (int)Scripts.ScriptCost.free, true, null, null, null, (int)Scripts.SortBy.views, (int)Scripts.Order.desc)]
15+
16+
// Paid scripts, filtered by key + verified
17+
[InlineData(2, 15, (int)Scripts.ScriptCost.paid, false, true, true, true, (int)Scripts.SortBy.likeCount, (int)Scripts.Order.asc)]
18+
19+
// Universal only, sorted by updatedAt
20+
[InlineData(1, 20, null, null, null, true, null, (int)Scripts.SortBy.updatedAt, (int)Scripts.Order.desc)]
21+
22+
// Verified scripts, sorted by createdAt ascending
23+
[InlineData(3, 7, null, null, null, null, true, (int)Scripts.SortBy.createdAt, (int)Scripts.Order.asc)]
24+
25+
// Filtered by dislike count (low value edge case for page/max)
26+
[InlineData(1, 1, null, null, null, null, null, (int)Scripts.SortBy.dislikeCount, (int)Scripts.Order.desc)]
27+
public async Task FetchScriptsAsync_WithVariants_ReturnsResults(
28+
int page,
29+
int max,
30+
int? mode,
31+
bool? patched,
32+
bool? key,
33+
bool? universal,
34+
bool? verified,
35+
int? sortBy,
36+
int? order
37+
)
38+
{
39+
Results? result = await Scripts.FetchScriptsAsync(
40+
page: page,
41+
max: max,
42+
mode: mode.HasValue ? (Scripts.ScriptCost?)mode.Value : null,
43+
patched: patched,
44+
key: key,
45+
universal: universal,
46+
verified: verified,
47+
sortBy: sortBy.HasValue ? (Scripts.SortBy?)sortBy.Value : null,
48+
order: order.HasValue ? (Scripts.Order?)order.Value : null
49+
);
50+
51+
Assert.NotNull(result);
52+
Assert.IsType<Results>(result);
53+
Assert.True(result.Scripts?.Count >= 0); // May be empty, but should be valid
54+
}
55+
56+
[Fact]
57+
public async Task FetchScriptAsync_ReturnsScriptData()
58+
{
59+
ScriptData? script = await Scripts.FetchScriptAsync("61abb678404510c9285337ec"); // https://scriptblox.com/script/GUI_57
60+
Assert.NotNull(script);
61+
Assert.IsType<ScriptData>(script);
62+
}
63+
64+
[Fact]
65+
public async Task FetchTrendingScriptsAsync_ReturnsList()
66+
{
67+
IReadOnlyList<Script>? scripts = await Scripts.FetchTrendingScriptsAsync();
68+
Assert.NotEmpty(scripts);
69+
}
70+
71+
[Fact]
72+
public async Task SearchScriptsAsync_ReturnsResults()
73+
{
74+
Results? result = await Scripts.SearchScriptsAsync("Infinite Yield");
75+
Assert.NotNull(result);
76+
Assert.IsType<Results>(result);
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)