Skip to content
Closed
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
110 changes: 110 additions & 0 deletions SlackNet.Tests/ExtraPropertiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Linq;
using System.Threading.Tasks;
using EasyAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using SlackNet.Blocks;
using SlackNet.Events;

namespace SlackNet.Tests;

public class ExtraPropertiesTests
{
private TestHttp _http;
private ISlackApiClient _api;
private JsonSerializerSettings _sut;

[SetUp]
public void SetUp()
{
var jsonSettings = Default.JsonSettings(Default.SlackTypeResolver(Default.AssembliesContainingSlackTypes));
_sut = jsonSettings.SerializerSettings;
_http = new TestHttp();
_api = new SlackApiClient(_http, Default.UrlBuilder(jsonSettings), jsonSettings, "token");
}

[Test]
public async Task User_PropertiesSlackNetDoesNotKnowAbout_AreInExtraProperties()
{
_http.Response = JObject.Parse(@"{
""ok"": true,
""user"": {
""id"": ""U12345678"",
""a_flag_slack_adds_later"": true,
""profile"": { ""email"": ""ada@example.com"", ""a_string_slack_adds_later"": ""foo"" },
""enterprise_user"": { ""id"": ""W12345678"", ""a_number_slack_adds_later"": 42 }
}
}");

var user = await _api.Users.Info("U12345678");

user.Id.ShouldBe("U12345678");
user.ExtraProperties["a_flag_slack_adds_later"].ShouldBe(true);
user.Profile.Email.ShouldBe("ada@example.com");
user.Profile.ExtraProperties["a_string_slack_adds_later"].ShouldBe("foo");
user.EnterpriseUser.ExtraProperties["a_number_slack_adds_later"].ShouldBe(42L);
}

[Test]
public async Task User_WithOnlyPropertiesSlackNetKnows_HasNoExtraProperties()
{
_http.Response = JObject.Parse(@"{ ""ok"": true, ""user"": { ""id"": ""U12345678"", ""is_admin"": true, ""profile"": { ""email"": ""ada@example.com"" } } }");

var user = await _api.Users.Info("U12345678");

user.ExtraProperties.ShouldBeEmpty();
user.Profile.ExtraProperties.ShouldBeEmpty();
}

[Test]
public async Task Conversation_PropertiesSlackNetDoesNotKnowAbout_AreInExtraProperties()
{
_http.Response = JObject.Parse(@"{
""ok"": true,
""channel"": {
""id"": ""C12345678"",
""a_flag_slack_adds_later"": true,
""topic"": { ""value"": ""Hello"", ""a_string_slack_adds_later"": ""foo"" }
}
}");

var conversation = await _api.Conversations.Info("C12345678");

conversation.ExtraProperties["a_flag_slack_adds_later"].ShouldBe(true);
conversation.Topic.ExtraProperties["a_string_slack_adds_later"].ShouldBe("foo");
}

[Test]
public void Block_PropertiesSlackNetDoesNotKnowAbout_AreInExtraProperties()
{
var result = Deserialize<Event>(@"{
""type"": ""message"",
""blocks"": [
{
""type"": ""section"",
""text"": { ""type"": ""mrkdwn"", ""text"": ""Hello"", ""a_flag_slack_adds_later"": true },
""a_string_slack_adds_later"": ""foo""
}
]
}");

var section = result.ShouldBeA<MessageEvent>().And.Blocks.Single().ShouldBeA<SectionBlock>().And;
section.ExtraProperties["a_string_slack_adds_later"].ShouldBe("foo");
section.Text.ExtraProperties["a_flag_slack_adds_later"].ShouldBe(true);
}

[Test]
public void ExtraProperties_AreNotSentBackToSlack()
{
var block = Deserialize<Block>(@"{ ""type"": ""section"", ""text"": { ""type"": ""plain_text"", ""text"": ""Hello"" }, ""a_string_slack_adds_later"": ""foo"" }");
block.ExtraProperties.Keys.ShouldContain("a_string_slack_adds_later");

var json = JObject.Parse(JsonConvert.SerializeObject(block, _sut));

json.Value<string>("type").ShouldBe("section");
json.ContainsKey("a_string_slack_adds_later").ShouldBe(false);
}

private T Deserialize<T>(string json) => JsonConvert.DeserializeObject<T>(json, _sut);
}
63 changes: 63 additions & 0 deletions SlackNet.Tests/ModelLintTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EasyAssertions;
using Newtonsoft.Json;
using NUnit.Framework;
using SlackNet.Events;
using SlackNet.SocketMode;
using SlackNet.WebApi;

namespace SlackNet.Tests;

public class ModelLintTests
{
[Test]
public void AllModelsHaveACatchAllForUnknownProperties()
{
ModelTypes
.Where(t => !t.IsAssignableTo(typeof(SlackObject)) && !WithTheirOwnCatchAll.Any(t.IsAssignableTo))
.ShouldBeEmpty("Models should derive from SlackObject, so that nothing Slack adds to them is lost");
}

[Test]
public void UnknownPropertiesAreReadButNotWritten()
{
var settings = Default.JsonSettings().SerializerSettings;

ModelTypes
.Where(t => t.IsAssignableTo(typeof(SlackObject)))
.Where(t =>
{
var model = (SlackObject)JsonConvert.DeserializeObject(@"{ ""not_modelled_yet"": ""value"" }", t, settings);
return model?.ExtraProperties.ContainsKey("not_modelled_yet") != true
|| JsonConvert.SerializeObject(model, settings).Contains("not_modelled_yet");
})
.ShouldBeEmpty("Unknown properties should end up in ExtraProperties, and not be written back when the model is sent to Slack");
}

// Everything Json.NET can fill in from a JSON object
private static IEnumerable<Type> ModelTypes => typeof(SlackObject).Assembly
.GetExportedTypes()
.Where(t => t is { IsClass: true, IsAbstract: false, IsGenericTypeDefinition: false }
&& t.GetConstructor(Type.EmptyTypes) != null
&& t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.SetMethod?.IsPublic == true && p.GetIndexParameters().Length == 0))
.Except(NotModels);

private static readonly Type[] NotModels =
[
typeof(SlackRequestContext), // Local context for the request being handled
typeof(SocketModeConnectionOptions), // Local configuration
];

// These have extension data of their own, which Json.NET fills instead of the one on SlackObject, so they don't derive from it
private static readonly Type[] WithTheirOwnCatchAll =
[
typeof(Event), // Also written back, which it always has been
typeof(MessageSearchResult), // Same as Event
typeof(Icons), // Images are what's left over after the known properties
typeof(ErrorResponse), // Info is what's left over after the known properties
typeof(WebApiResponse), // Data is the whole response
];
}
2 changes: 1 addition & 1 deletion SlackNet/Blocks/Block.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace SlackNet.Blocks;

public abstract class Block(string type)
public abstract class Block(string type) : SlackObject
{
public string Type { get; } = type;

Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/BlockAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace SlackNet.Blocks;

public class BlockAction
public class BlockAction : SlackObject
{
public string Type { get; set; }
public string ActionId { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/BlockElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public interface IBlockElement
string Type { get; }
}

public abstract class BlockElement(string type) : IBlockElement
public abstract class BlockElement(string type) : SlackObject, IBlockElement
{
public string Type { get; } = type;
}
2 changes: 1 addition & 1 deletion SlackNet/Blocks/ConfirmationDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// An object that defines a dialog that provides a confirmation step to any interactive element.
/// This dialog will ask the user to confirm their action by offering a confirm and deny buttons.
/// </summary>
public class ConfirmationDialog
public class ConfirmationDialog : SlackObject
{
/// <summary>
/// A plain text object that defines the dialog's title.
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/ContextActionsBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class ContextActionsBlock() : Block("context_actions")
public IList<ContextActionsElement> Elements { get; set; } = [];
}

public abstract class ContextActionsElement(string type)
public abstract class ContextActionsElement(string type) : SlackObject
{
public string Type { get; set; } = type;
public string ActionId { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/ConversationFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SlackNet.Blocks;
/// <summary>
/// Provides a way to filter the list of options in a <see cref="ConversationSelectMenu"/> or <see cref="ConversationMultiSelectMenu"/>.
/// </summary>
public class ConversationFilter
public class ConversationFilter : SlackObject
{
/// <summary>
/// Indicates which type of conversations should be included in the list.
Expand Down
10 changes: 5 additions & 5 deletions SlackNet/Blocks/DataVisualizationBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DataVisualizationBlock() : Block("data_visualization")
/// <summary>
/// A chart displayed by a <see cref="DataVisualizationBlock"/>.
/// </summary>
public abstract class DataVisualizationChart(string type)
public abstract class DataVisualizationChart(string type) : SlackObject
{
/// <summary>
/// The type of chart.
Expand Down Expand Up @@ -80,7 +80,7 @@ public class LineChart() : AxisChart("line");
/// <summary>
/// A labeled slice in a <see cref="PieChart"/>.
/// </summary>
public class ChartSegment
public class ChartSegment : SlackObject
{
/// <summary>
/// Display name for this slice, shown in the legend and on hover.
Expand All @@ -96,7 +96,7 @@ public class ChartSegment
/// <summary>
/// A named set of ordered data points.
/// </summary>
public class DataSeries
public class DataSeries : SlackObject
{
/// <summary>
/// Human-readable identifier displayed in the chart legend.
Expand All @@ -112,7 +112,7 @@ public class DataSeries
/// <summary>
/// A value belonging to an x-axis category.
/// </summary>
public class DataPoint
public class DataPoint : SlackObject
{
/// <summary>
/// The x-axis category this point belongs to.
Expand All @@ -128,7 +128,7 @@ public class DataPoint
/// <summary>
/// X-axis categories and optional axis titles.
/// </summary>
public class AxisConfig
public class AxisConfig : SlackObject
{
/// <summary>
/// Category labels for the x-axis, in display order.
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/DispatchActionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SlackNet.Blocks;
/// <summary>
/// Determines when a<see cref="PlainTextInput"/> element will return a <see cref="BlockAction"/> interaction payload.
/// </summary>
public class DispatchActionConfig
public class DispatchActionConfig : SlackObject
{
/// <summary>
/// An array of <see cref="InteractionType"/> that you would like to receive a <see cref="BlockAction"/> payload for.
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/FeedbackButtons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class FeedbackButtons() : ContextActionsElement("feedback_buttons")
public FeedbackButton NegativeButton { get; set; } = new();
}

public class FeedbackButton
public class FeedbackButton : SlackObject
{
public PlainText Text { get; set; }
public string Value { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/ImageFileReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// Defines an object containing Slack file information to be used in an image block or image element.
/// </summary>
public class ImageFileReference
public class ImageFileReference : SlackObject
{
/// <summary>
/// This URL can be the <see cref="File.UrlPrivate"/> or the <see cref="File.Permalink"/> of the Slack <see cref="File"/>.
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/Option.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// An object that represents a single selectable item in a select menu.
/// </summary>
public class Option
public class Option : SlackObject
{
/// <summary>
/// A plain text object that defines the text shown in the option on the menu.
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/OptionGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SlackNet.Blocks;
/// <summary>
/// Provides a way to group options in a select menu.
/// </summary>
public class OptionGroup
public class OptionGroup : SlackObject
{
/// <summary>
/// A plain text object that defines the label shown above this group of options.
Expand Down
6 changes: 3 additions & 3 deletions SlackNet/Blocks/RichTextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class RichTextBlock() : Block("rich_text")
public IList<RichTextElement> Elements { get; set; } = [];
}

public abstract class RichTextElement(string type)
public abstract class RichTextElement(string type) : SlackObject
{
public string Type { get; set; } = type;
}
Expand All @@ -32,7 +32,7 @@ public class RichTextSection() : RichTextElement("rich_text_section")
public IList<RichTextSectionElement> Elements { get; set; } = [];
}

public abstract class RichTextSectionElement(string type)
public abstract class RichTextSectionElement(string type) : SlackObject
{
public string Type { get; set; } = type;
}
Expand Down Expand Up @@ -288,7 +288,7 @@ public class RichTextListRecord() : RichTextElement("list_record")
/// <summary>
/// Rich text styling. Note that not all properties are supported by all elements.
/// </summary>
public class RichTextStyle
public class RichTextStyle : SlackObject
{
[IgnoreIfDefault]
public bool Bold { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/SlackIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SlackNet.Blocks;
/// Defines Slack icon information for use in a <see cref="CardBlock"/>.
/// </summary>
/// <remarks>See the <a href="https://docs.slack.dev/reference/block-kit/composition-objects/slack-icon-object/">Slack documentation</a> for more information.</remarks>
public class SlackIcon
public class SlackIcon : SlackObject
{
/// <summary>
/// The type of composition object. Always <c>icon</c>.
Expand Down
4 changes: 2 additions & 2 deletions SlackNet/Blocks/TableBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class TableBlock() : Block("table")
public IList<TableColumnSettings> ColumnSettings { get; set; } = [];
}

public abstract class TableCell(string type)
public abstract class TableCell(string type) : SlackObject
{
public string Type { get; set; } = type;
}
Expand Down Expand Up @@ -38,7 +38,7 @@ public class RawNumberCell() : TableCell("raw_number")
public string Text { get; set; }
}

public class TableColumnSettings
public class TableColumnSettings : SlackObject
{
public ColumnAlignment Align { get; set; }
public bool IsWrapped { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/TaskCardBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class TaskCardBlock() : Block("task_card")
/// <summary>
/// One of Slack's named task icons.
/// </summary>
public class TaskCardIcon
public class TaskCardIcon : SlackObject
{
public string Type { get; set; } = "icon";

Expand Down
2 changes: 1 addition & 1 deletion SlackNet/Blocks/TextObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// An object containing some text, formatted either as plain text or using Slack's "mrkdwn".
/// </summary>
public abstract class TextObject
public abstract class TextObject : SlackObject
{
protected TextObject(string type)
{
Expand Down
Loading
Loading