1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-09-14 01:32:10 +02:00

Compare commits

..

18 Commits

Author SHA1 Message Date
1a31e69ec9 Release 1.14.4.1 2018-06-29 19:56:00 +02:00
e065983c95 Delay the apocalypse (freeze TweetDeck JS resources, force update checking) 2018-06-29 19:41:02 +02:00
30a169171a Apparently deserializing @TryMyAwesomeApp sometimes causes a hidden crash 2018-06-29 17:10:11 +02:00
8d1900362e Update PostCefUpdate script to remove invalid build configurations (x64, AnyCPU) 2018-06-29 17:05:53 +02:00
e154189de1 Cleanup TweetDuck.csproj and fix names in TestResult unit test 2018-06-28 14:06:03 +02:00
b0f147de24 Fix misaligned 'Add column' icons when using old icon design 2018-06-28 08:10:48 +02:00
979b3548db Release 1.14.4 2018-06-28 07:54:38 +02:00
05d6c578b3 Move InjectedHTML unit tests to xUnit and rename Inject method 2018-06-26 11:19:44 +02:00
a117559063 Minor formatting tweaks 2018-06-26 10:05:53 +02:00
f87c649b09 Fix Twitter experiment causing crash in notifications and subsequent render corruption 2018-06-26 09:49:37 +02:00
6504dc9184 Add unit tests for Result and a few utility methods & fix edge case in StringUtils 2018-06-24 21:41:02 +02:00
25a8ddffd4 Rewrite and tweak existing Core namespace unit tests into xUnit 2018-06-24 19:29:24 +02:00
fa0f9b89cf Remove 'Release' configuration from UnitTests project 2018-06-24 16:16:11 +02:00
4d00a67891 Add a new F# xUnit test project 2018-06-24 16:09:21 +02:00
bd2c43e1f4 Fix analytics not counting applying ROT13 on non-editable text 2018-06-19 21:35:35 +02:00
c7279eaa34 Fix bug with falsely detecting symlinks in plugins if a file/folder doesn't exist 2018-06-19 21:32:21 +02:00
fd523e552c Symlinks/junctions in plugin folders can go to hell 2018-06-13 22:20:10 +02:00
cb877b8b23 Fix broken desktop notifications for retweets with sensitive media 2018-06-10 23:53:01 +02:00
31 changed files with 711 additions and 308 deletions

View File

@@ -393,7 +393,7 @@ namespace TweetDuck.Core{
FormSettings form = new FormSettings(this, plugins, updates, analytics, startTab);
form.FormClosed += (sender, args) => {
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck && !UpdateHandler.TemporarilyForceUpdateChecking){
Config.DismissedUpdate = null;
Config.Save();

View File

@@ -196,6 +196,7 @@ namespace TweetDuck.Core.Handling{
case MenuReadApplyROT13:
string selection = parameters.SelectionText;
control.InvokeAsyncSafe(() => FormMessage.Information("ROT13", StringUtils.ConvertRot13(selection), FormMessage.OK));
control.InvokeAsyncSafe(analytics.AnalyticsFile.UsedROT13.Trigger);
return true;
case MenuSearchInBrowser:

View File

@@ -1,8 +1,11 @@
using CefSharp;
using System.Text.RegularExpressions;
using CefSharp;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
sealed class RequestHandlerBrowser : RequestHandlerBase{
private static readonly Regex TmpResourceRedirect = new Regex(@"dist\/(.*?)\.(.*?)\.js$", RegexOptions.Compiled);
public string BlockNextUserNavUrl { get; set; }
public RequestHandlerBrowser() : base(true){}
@@ -31,6 +34,30 @@ namespace TweetDuck.Core.Handling{
request.Url = TwitterUtils.LoadingSpinner.Url;
return true;
}
else if (request.ResourceType == ResourceType.Script){ // TODO delaying the apocalypse
Match match = TmpResourceRedirect.Match(request.Url);
if (match.Success){
string scriptType = match.Groups[1].Value;
string scriptHash = match.Groups[2].Value;
const string HashBundle = "1bd75b5854";
const string HashVendor = "942c0a20e8";
if (scriptType == "bundle" && scriptHash != HashBundle){
request.Url = TmpResourceRedirect.Replace(request.Url, "dist/$1."+HashBundle+".js");
System.Diagnostics.Debug.WriteLine("rewriting "+scriptType+" to "+request.Url);
return true;
}
else if (scriptType == "vendor" && scriptHash != HashVendor){
request.Url = TmpResourceRedirect.Replace(request.Url, "dist/$1."+HashVendor+".js");
System.Diagnostics.Debug.WriteLine("rewriting "+scriptType+" to "+request.Url);
return true;
}
System.Diagnostics.Debug.WriteLine("accepting "+scriptType+" as "+request.Url);
}
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}

View File

@@ -263,7 +263,7 @@ namespace TweetDuck.Core.Notification{
string html = base.GetTweetHTML(tweet);
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.Inject(html);
html = injection.InjectInto(html);
}
return html;

View File

@@ -47,7 +47,7 @@ namespace TweetDuck.Core.Notification.Screenshot{
string html = tweet.GenerateHtml("td-screenshot");
foreach(InjectedHTML injection in plugins.NotificationInjections){
html = injection.Inject(html);
html = injection.InjectInto(html);
}
return html;

View File

@@ -28,10 +28,14 @@ namespace TweetDuck.Core.Utils{
}
public static int CountOccurrences(string source, string substring){
int count = 0, index = 0;
if (substring.Length == 0){
throw new ArgumentOutOfRangeException(nameof(substring), "Searched substring must not be empty.");
}
int count = 0, index = 0, length = substring.Length;
while((index = source.IndexOf(substring, index)) != -1){
index += substring.Length;
index += length;
++count;
}

View File

@@ -16,7 +16,7 @@ namespace TweetDuck.Data{
this.html = html;
}
public string Inject(string targetHTML){
public string InjectInto(string targetHTML){
int index = targetHTML.IndexOf(search, StringComparison.Ordinal);
if (index == -1){

View File

@@ -84,14 +84,20 @@ namespace TweetDuck.Plugins{
try{
string folderPathName = new DirectoryInfo(rootFolder).FullName;
DirectoryInfo currentInfo = new DirectoryInfo(fullPath);
DirectoryInfo currentInfo = new DirectoryInfo(fullPath); // initially points to the file, which is convenient for the Attributes check below
DirectoryInfo parentInfo = currentInfo.Parent;
while(currentInfo.Parent != null){
if (currentInfo.Parent.FullName == folderPathName){
while(parentInfo != null){
if (currentInfo.Exists && currentInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)){
return string.Empty; // no reason why a plugin should have files/folders with symlinks, junctions, or any other crap
}
if (parentInfo.FullName == folderPathName){
return fullPath;
}
currentInfo = currentInfo.Parent;
currentInfo = parentInfo;
parentInfo = currentInfo.Parent;
}
}
catch{

View File

@@ -19,7 +19,7 @@ namespace TweetDuck{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.14.3";
public const string VersionTag = "1.14.4.1";
public static readonly string ProgramPath = AppDomain.CurrentDomain.BaseDirectory;
public static readonly bool IsPortable = File.Exists(Path.Combine(ProgramPath, "makeportable"));

View File

@@ -42,5 +42,6 @@ using TweetDuck;
[assembly: CLSCompliant(true)]
#if DEBUG
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("TweetTest.Unit")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
#endif

View File

@@ -9,8 +9,9 @@
The program was built using Visual Studio 2017. Before opening the solution, please make sure you have the following workloads and components installed (optional components that are not listed can be deselected to save space):
* **.NET desktop development**
* .NET Framework 4 4.6 development tools
* F# desktop language support
* **Desktop development with C++**
* VC++ 2017 v141 toolset
* VC++ 2017 latest v141 tools
After opening the solution, right-click the solution and select **Restore NuGet Packages**, or manually run this command in the **Package Manager Console**:
```
@@ -27,7 +28,7 @@ While debugging, opening the main menu and clicking **Reload browser** automatic
### Release
Open **Batch Build**, tick all `Release` configurations with `x86` platform except for the `UnitTest` project, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
Open **Batch Build**, tick all `Release` configurations with `x86` platform, and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
After the build succeeds, the `bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers) section for automated installer generation.
@@ -63,6 +64,8 @@ After the window closes, three installers will be generated inside the `bld/Outp
#### Notes
> When opening **Batch Build**, you will also see `x64` and `AnyCPU` configurations. These are visible due to what I consider a Visual Studio bug, and will not work without significant changes to the project. Manually running the `Resources/PostCefUpdate.ps1` PowerShell script modifies the downloaded CefSharp packages, and removes the invalid configurations.
> There is a small chance running `RUN BUILD.bat` immediately shows a resource error. If that happens, close the console window (which terminates all Inno Setup processes and leaves corrupted installers in the output folder), and run it again.
> Running `RUN BUILD.bat` uses about 400 MB of RAM due to high compression. You can lower this to about 140 MB by opening `gen_full.iss` and `gen_port.iss`, and changing `LZMADictionarySize=15360` to `LZMADictionarySize=4096`.

View File

@@ -541,7 +541,7 @@ ${iconData.map(entry => `#tduck .icon-${entry[0]}:before{content:\"\\f0${entry[1
#tduck .js-docked-compose .js-drawer-close { margin: 20px 0 0 !important }
#tduck .search-input-control .icon { font-size: 20px !important; top: -4px !important }
.column-type-icon { margin-top: -1px !important }
.js-column-header .column-type-icon { margin-top: -1px !important }
.inline-reply .pull-left .Button--link { margin-top: 3px !important }
.tweet-action-item .icon-favorite-toggle { font-size: 16px !important; }

View File

@@ -9,14 +9,13 @@ $ErrorActionPreference = "Stop"
try{
$sw = [Diagnostics.Stopwatch]::StartNew()
Write-Host "--------------------------"
if ($version.Equals("")){
$version = (Get-Item (Join-Path $targetDir "TweetDuck.exe")).VersionInfo.FileVersion
}
Write-Host "--------------------------"
Write-Host "TweetDuck version" $version
Write-Host "--------------------------"
# Cleanup
@@ -62,7 +61,7 @@ try{
}
if ((Get-Content -Path $file -Raw).Contains("`r")){
Throw "$file cannot contain carriage return"
Throw "$file must not have any carriage return characters"
}
Write-Host "Verified" $file.Substring($targetDir.Length)
@@ -116,10 +115,13 @@ try{
Rewrite-File $file $lines
}
Write-Host "------------------"
# Finished
$sw.Stop()
Write-Host "------------------"
Write-Host "Finished in" $([math]::Round($sw.Elapsed.TotalMilliseconds)) "ms"
Write-Host "------------------"
}catch{
Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber
Write-Host $_

View File

@@ -1,18 +1,60 @@
$ErrorActionPreference = "Stop"
$MainProj = "..\TweetDuck.csproj"
$BrowserProj = "..\subprocess\TweetDuck.Browser.csproj"
try{
$mainProj = "..\TweetDuck.csproj"
$browserProj = "..\subprocess\TweetDuck.Browser.csproj"
$Match = Select-String -Path $MainProj '<Import Project="packages\\cef\.redist\.x86\.(.*?)\\'
$Version = $Match.Matches[0].Groups[1].Value
$cefMatch = Select-String -Path $mainProj '<Import Project="packages\\cef\.redist\.x86\.(.*?)\\'
$cefVersion = $cefMatch.Matches[0].Groups[1].Value
Copy-Item "..\packages\cef.redist.x86.${Version}\CEF\devtools_resources.pak" -Destination "..\bld\Resources\" -Force
$sharpMatch = Select-String -Path $mainProj '<Import Project="packages\\CefSharp\.Common\.(.*?)\\'
$sharpVersion = $sharpMatch.Matches[0].Groups[1].Value
$Match = Select-String -Path $MainProj '<Import Project="packages\\CefSharp\.Common\.(.*?)\\'
$Version = $Match.Matches[0].Groups[1].Value
$propsFiles = "..\packages\CefSharp.Common.${sharpVersion}\build\CefSharp.Common.props",
"..\packages\CefSharp.WinForms.${sharpVersion}\build\CefSharp.WinForms.props"
$Contents = [IO.File]::ReadAllText($BrowserProj)
$Contents = $Contents -Replace '(?<=<HintPath>\.\.\\packages\\CefSharp\.Common\.)(.*?)(?=\\)', $Version
$Contents = $Contents -Replace '(?<=<Reference Include="CefSharp\.BrowserSubprocess\.Core, Version=)(\d+)', $Version.Split(".")[0]
# Greetings
[IO.File]::WriteAllText($BrowserProj, $Contents)
$title = "CEF ${cefVersion}, CefSharp ${sharpVersion}"
Write-Host ("-" * $title.Length)
Write-Host $title
Write-Host ("-" * $title.Length)
# Perform update
Write-Host "Copying dev tools to repository..."
Copy-Item "..\packages\cef.redist.x86.${cefVersion}\CEF\devtools_resources.pak" -Destination "..\bld\Resources\" -Force
Write-Host "Updating browser subprocess reference..."
$contents = [IO.File]::ReadAllText($browserProj)
$contents = $contents -Replace '(?<=<HintPath>\.\.\\packages\\CefSharp\.Common\.)(.*?)(?=\\)', $sharpVersion
$contents = $contents -Replace '(?<=<Reference Include="CefSharp\.BrowserSubprocess\.Core, Version=)(\d+)', $sharpVersion.Split(".")[0]
[IO.File]::WriteAllText($browserProj, $contents)
Write-Host "Removing x64 and AnyCPU from CefSharp props..."
foreach($file in $propsFiles){
$contents = [IO.File]::ReadAllText($file)
$contents = $contents -Replace '(?<=<When Condition=")(''\$\(Platform\)'' == ''(AnyCPU|x64)'')(?=">)', 'false'
[IO.File]::WriteAllText($file, $contents)
}
# Finished
Write-Host ""
Write-Host "Finished. Exiting in 6 seconds..."
Start-Sleep -Seconds 6
}catch{
Write-Host ""
Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber
Write-Host $_
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Exit 1
}

View File

@@ -145,6 +145,19 @@
return false;
};
let isSensitive = (tweet) => {
let main = tweet.getMainTweet && tweet.getMainTweet();
return true if main && main.possiblySensitive; // TODO these don't show media badges when hiding sensitive media
let related = tweet.getRelatedTweet && tweet.getRelatedTweet();
return true if related && related.possiblySensitive;
let quoted = tweet.quotedTweet;
return true if quoted && quoted.possiblySensitive;
return false;
};
let fixMedia = (html, media) => {
return html.find("a[data-media-entity-id='"+media.mediaId+"'], .media-item").first().removeClass("is-zoomable").css("background-image", 'url("'+media.small()+'")');
};
@@ -160,7 +173,7 @@
startRecentTweetTimer();
if (column.model.getHasNotification()){
let sensitive = (tweet.getRelatedTweet() && tweet.getRelatedTweet().possiblySensitive || (tweet.quotedTweet && tweet.quotedTweet.possiblySensitive));
let sensitive = isSensitive(tweet);
let previews = $TDX.notificationMediaPreviews && (!sensitive || TD.settings.getDisplaySensitiveMedia());
let html = $(tweet.render({
@@ -172,7 +185,8 @@
isMediaPreviewLarge: false,
isMediaPreviewCompact: false,
isMediaPreviewInQuoted: previews,
thumbSizeClass: "media-size-medium"
thumbSizeClass: "media-size-medium",
mediaPreviewSize: "medium"
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
@@ -384,6 +398,8 @@
let menu = $(".js-dropdown-content").children("ul").first();
return if menu.length === 0;
menu.find(".update-available-item").parent().remove(); // TODO temp
let button = $('<li class="is-selectable" data-tweetduck><a href="#" data-action>TweetDuck</a></li>');
button.insertBefore(menu.children(".drp-h-divider").last());
@@ -512,6 +528,7 @@
obj.profileImageURL = realAvatar;
obj.url = "https://tweetduck.chylex.com";
if (obj.entities && obj.entities.url){
obj.entities.url.urls = [{
url: obj.url,
expanded_url: obj.url,
@@ -519,6 +536,7 @@
indices: [ 0, 23 ]
}];
}
}
else if (e && e.url && e.url.urls && e.url.urls.length && e.url.urls[0].expanded_url){
obj.url = e.url.urls[0].expanded_url;
}
@@ -1504,10 +1522,17 @@
//
// Block: Disable default TweetDeck update notification.
//
onAppReady.push(function(){
$(document).on("uiSuggestRefreshToggle", function(e){
e.stopPropagation();
e.stopImmediatePropagation();
});
/*onAppReady.push(function(){
let events = $._data(document, "events");
delete events["uiSuggestRefreshToggle"];
});
//$(".js-app-settings .icon-settings").removeClass("color-twitter-yellow"); // TODO temp
});*/
//
// Block: Disable TweetDeck metrics.

View File

@@ -164,7 +164,7 @@
<div class='tdu-buttons'>
<button class='tdu-btn-download'>Update now</button>
<button class='tdu-btn-later'>Remind me later</button>
<button class='tdu-btn-ignore'>Ignore this update</button>
<!-- TODO <button class='tdu-btn-ignore'>Ignore this update</button>-->
</div>
</div>
`).appendTo(document.body).css("display", existed ? "block" : "none");

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\CefSharp.WinForms.66.0.0-CI2629\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.66.0.0-CI2629\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.66.0.0-CI2629\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.66.0.0-CI2629\build\CefSharp.Common.props')" />
@@ -15,25 +15,8 @@
<AssemblyName>TweetDuck</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<TargetFrameworkProfile>
</TargetFrameworkProfile>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<StartArguments>-datafolder TweetDuckDebug</StartArguments>
@@ -47,23 +30,15 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>false</Prefer32Bit>
<UseVSHostingProcess>false</UseVSHostingProcess>
<LangVersion>7</LangVersion>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>bin\x86\Release\</OutputPath>
<Optimize>true</Optimize>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DefineConstants>
</DefineConstants>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup>
@@ -330,11 +305,6 @@
<Compile Include="Updates\Events\UpdateEventArgs.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4 Client Profile %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
@@ -345,11 +315,6 @@
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Windows.Installer.4.5">
<Visible>False</Visible>
<ProductName>Windows Installer 4.5</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Core\FormBrowser.resx">

View File

@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "video\Tw
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TweetTest.Unit", "lib\TweetTest.Unit\TweetTest.Unit.fsproj", "{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
@@ -28,7 +30,8 @@ Global
{B10B0017-819E-4F71-870F-8256B36A26AA}.Release|x86.Build.0 = Release|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.ActiveCfg = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Debug|x86.Build.0 = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|x86.ActiveCfg = Release|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|x86.ActiveCfg = Debug|x86
{A958FA7A-4A2C-42A7-BFA0-159343483F4E}.Release|x86.Build.0 = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.ActiveCfg = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Debug|x86.Build.0 = Debug|x86
{278B2D11-402D-44B6-B6A1-8FA67DB65565}.Release|x86.ActiveCfg = Release|x86
@@ -37,6 +40,10 @@ Global
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.Build.0 = Release|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.ActiveCfg = Debug|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Debug|x86.Build.0 = Debug|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.ActiveCfg = Debug|x86
{EEE1071A-28FA-48B1-82A1-9CBDC5C3F2C3}.Release|x86.Build.0 = Debug|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -10,6 +10,8 @@ using Timer = System.Windows.Forms.Timer;
namespace TweetDuck.Updates{
sealed class UpdateHandler : IDisposable{
public const bool TemporarilyForceUpdateChecking = true;
public const int CheckCodeUpdatesDisabled = -1;
public const int CheckCodeNotOnTweetDeck = -2;
@@ -53,7 +55,7 @@ namespace TweetDuck.Updates{
timer.Stop();
if (Program.UserConfig.EnableUpdateCheck){
if (Program.UserConfig.EnableUpdateCheck || TemporarilyForceUpdateChecking){
DateTime now = DateTime.Now;
TimeSpan nextHour = now.AddSeconds(60*(60-now.Minute)-now.Second)-now;
@@ -67,7 +69,7 @@ namespace TweetDuck.Updates{
}
public int Check(bool force){
if (Program.UserConfig.EnableUpdateCheck || force){
if (Program.UserConfig.EnableUpdateCheck || TemporarilyForceUpdateChecking || force){
if (!browser.IsTweetDeckWebsite){
return CheckCodeNotOnTweetDeck;
}

View File

@@ -0,0 +1,79 @@
namespace Unit.Core.BrowserUtils
open Xunit
open TweetDuck.Core.Utils
module CheckUrl =
type Result = BrowserUtils.UrlCheckResult
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://example.com"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("https://example.com"))
[<Fact>]
let ``accepts FTP protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("ftp://example.com"))
[<Fact>]
let ``accepts MAILTO protocol`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("mailto://someone@example.com"))
[<Fact>]
let ``accepts URL with port, path, query, and hash`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://www.example.co.uk:80/path?key=abc&array[]=5#hash"))
[<Fact>]
let ``accepts IPv4 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://127.0.0.1"))
[<Fact>]
let ``accepts IPv6 address`` () =
Assert.Equal(Result.Fine, BrowserUtils.CheckUrl("http://[2001:db8:0:0:0:ff00:42:8329]"))
[<Fact>]
let ``recognizes t.co as tracking URL`` () =
Assert.Equal(Result.Tracking, BrowserUtils.CheckUrl("http://t.co/12345"))
[<Fact>]
let ``rejects empty URL`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl(""))
[<Fact>]
let ``rejects missing protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("www.example.com"))
[<Fact>]
let ``rejects banned protocol`` () =
Assert.Equal(Result.Invalid, BrowserUtils.CheckUrl("file://example.com"))
module GetFileNameFromUrl =
[<Fact>]
let ``simple file URL returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html"))
[<Fact>]
let ``file URL with query returns file name`` () =
Assert.Equal("index.html", BrowserUtils.GetFileNameFromUrl("http://example.com/index.html?version=2"))
[<Fact>]
let ``file URL w/o extension returns file name`` () =
Assert.Equal("index", BrowserUtils.GetFileNameFromUrl("http://example.com/index"))
[<Fact>]
let ``file URL with trailing dot returns file name with dot`` () =
Assert.Equal("index.", BrowserUtils.GetFileNameFromUrl("http://example.com/index."))
[<Fact>]
let ``root URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com"))
[<Fact>]
let ``path URL returns null`` () =
Assert.Null(BrowserUtils.GetFileNameFromUrl("http://example.com/path/"))

View File

@@ -0,0 +1,118 @@
namespace Unit.Core.StringUtils
open Xunit
open TweetDuck.Core.Utils
module ExtractBefore =
[<Fact>]
let ``empty input string returns empty string`` () =
Assert.Equal("", StringUtils.ExtractBefore("", ' '))
[<Fact>]
let ``input string w/o searched char returns input string`` () =
Assert.Equal("abc", StringUtils.ExtractBefore("abc", ' '))
[<Fact>]
let ``finding searched char returns everything before it`` () =
Assert.Equal("abc", StringUtils.ExtractBefore("abc def ghi", ' '))
[<Theory>]
[<InlineData(0, "abc")>]
[<InlineData(3, "abc")>]
[<InlineData(4, "abc def")>]
[<InlineData(7, "abc def")>]
[<InlineData(8, "abc def ghi")>]
let ``start index works`` (startIndex: int, expectedValue: string) =
Assert.Equal(expectedValue, StringUtils.ExtractBefore("abc def ghi", ' ', startIndex))
module ParseInts =
open System
[<Fact>]
let ``empty input string returns empty collection`` () =
Assert.Empty(StringUtils.ParseInts("", ','))
[<Fact>]
let ``single integer is parsed correctly`` () =
Assert.Equal([ 1 ], StringUtils.ParseInts("1", ','))
[<Fact>]
let ``multiple integers are parsed correctly`` () =
Assert.Equal([ -3 .. 3 ], StringUtils.ParseInts("-3,-2,-1,0,1,2,3", ','))
[<Fact>]
let ``excessive delimiters are discarded`` () =
Assert.Equal([ 1 .. 4 ], StringUtils.ParseInts(",,1,,,2,3,,4,,,", ','))
[<Fact>]
let ``invalid format throws exception`` () =
Assert.Throws<FormatException>(fun () -> StringUtils.ParseInts("1,2,a", ',') |> ignore)
module ConvertPascalCaseToScreamingSnakeCase =
[<Fact>]
let ``converts one word`` () =
Assert.Equal("HELP", StringUtils.ConvertPascalCaseToScreamingSnakeCase("Help"))
[<Fact>]
let ``converts two words`` () =
Assert.Equal("HELP_ME", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMe"))
[<Fact>]
let ``converts many words`` () =
Assert.Equal("HELP_ME_PLEASE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMePlease"))
[<Fact>]
let ``converts one uppercase abbreviation`` () =
Assert.Equal("HTML_CODE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HTMLCode"))
[<Fact>]
let ``converts many uppercase abbreviations`` () =
Assert.Equal("I_LIKE_HTML_AND_CSS", StringUtils.ConvertPascalCaseToScreamingSnakeCase("ILikeHTMLAndCSS"))
module ConvertRot13 =
[<Fact>]
let ``ignores digits and special characters`` () =
Assert.Equal("<123'456.789>", StringUtils.ConvertRot13("<123'456.789>"))
[<Fact>]
let ``converts lowercase letters correctly`` () =
Assert.Equal("nopqrstuvwxyzabcdefghijklm", StringUtils.ConvertRot13("abcdefghijklmnopqrstuvwxyz"))
[<Fact>]
let ``converts uppercase letters correctly`` () =
Assert.Equal("NOPQRSTUVWXYZABCDEFGHIJKLM", StringUtils.ConvertRot13("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
[<Fact>]
let ``converts mixed character types correctly`` () =
Assert.Equal("Uryyb, jbeyq! :)", StringUtils.ConvertRot13("Hello, world! :)"))
module CountOccurrences =
open System
[<Fact>]
let ``empty input string returns zero`` () =
Assert.Equal(0, StringUtils.CountOccurrences("", "a"))
[<Fact>]
let ``empty searched string throws`` () =
Assert.Throws<ArgumentOutOfRangeException>(fun () -> StringUtils.CountOccurrences("hello", "") |> ignore)
[<Fact>]
let ``counts single letter characters correctly`` () =
Assert.Equal(3, StringUtils.CountOccurrences("hello world", "l"))
[<Fact>]
let ``counts longer substrings correctly`` () =
Assert.Equal(2, StringUtils.CountOccurrences("hello and welcome in hell", "hell"))
[<Fact>]
let ``does not count overlapping substrings`` () =
Assert.Equal(2, StringUtils.CountOccurrences("aaaaa", "aa"))

View File

@@ -0,0 +1,112 @@
namespace Unit.Core.TwitterUtils
open Xunit
open TweetDuck.Core.Utils
[<Collection("RegexAccount")>]
module RegexAccount_IsMatch =
let isMatch = TwitterUtils.RegexAccount.IsMatch
[<Fact>]
let ``accepts HTTP protocol`` () =
Assert.True(isMatch("http://twitter.com/chylexmc"))
[<Fact>]
let ``accepts HTTPS protocol`` () =
Assert.True(isMatch("https://twitter.com/chylexmc"))
[<Fact>]
let ``accepts trailing slash`` () =
Assert.True(isMatch("https://twitter.com/chylexmc/"))
[<Fact>]
let ``rejects URL with query`` () =
Assert.False(isMatch("https://twitter.com/chylexmc?query"))
[<Fact>]
let ``rejects URL with extra path`` () =
Assert.False(isMatch("https://twitter.com/chylexmc/status/123"))
[<Theory>]
[<InlineData("signup")>]
[<InlineData("tos")>]
[<InlineData("privacy")>]
[<InlineData("search")>]
[<InlineData("search?query")>]
[<InlineData("search-home")>]
[<InlineData("search-advanced")>]
let ``rejects reserved page names`` (name: string) =
Assert.False(isMatch("https://twitter.com/"+name))
[<Theory>]
[<InlineData("tosser")>]
[<InlineData("searching")>]
let ``accepts accounts starting with reserved page names`` (name: string) =
Assert.True(isMatch("https://twitter.com/"+name))
[<Collection("RegexAccount")>]
module RegexAccount_Match =
let extract str = TwitterUtils.RegexAccount.Match(str).Groups.[1].Value
[<Fact>]
let ``extracts account name from simple URL`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123"))
[<Fact>]
let ``extracts account name from URL with trailing slash`` () =
Assert.Equal("_abc_DEF_123", extract("https://twitter.com/_abc_DEF_123/"))
module GetMediaLink_Default =
let getMediaLinkDefault url = TwitterUtils.GetMediaLink(url, TwitterUtils.ImageQuality.Default)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``does not modify URL w/o extension`` () =
Assert.Equal(domain+"/media/123", getMediaLinkDefault(domain+"/media/123"))
[<Fact>]
let ``does not modify URL w/o quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg", getMediaLinkDefault(domain+"/media/123.jpg"))
[<Fact>]
let ``does not modify URL with quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:small", getMediaLinkDefault(domain+"/media/123.jpg:small"))
module GetMediaLink_Orig =
let getMediaLinkOrig url = TwitterUtils.GetMediaLink(url, TwitterUtils.ImageQuality.Orig)
let domain = "https://pbs.twimg.com"
[<Fact>]
let ``appends :orig to valid URL w/o quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:orig", getMediaLinkOrig(domain+"/media/123.jpg"))
[<Fact>]
let ``rewrites :orig into valid URL with quality suffix`` () =
Assert.Equal(domain+"/media/123.jpg:orig", getMediaLinkOrig(domain+"/media/123.jpg:small"))
[<Fact>]
let ``does not modify unknown URL w/o quality suffix`` () =
Assert.Equal(domain+"/profile_images/123.jpg", getMediaLinkOrig(domain+"/profile_images/123.jpg"))
[<Fact>]
let ``rewrites :orig into unknown URL with quality suffix`` () =
Assert.Equal(domain+"/profile_images/123.jpg:orig", getMediaLinkOrig(domain+"/profile_images/123.jpg:small"))
module GetImageFileName =
[<Fact>]
let ``extracts file name from URL w/o quality suffix`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com/test.jpg"))
[<Fact>]
let ``extracts file name from URL with quality suffix`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com/test.jpg:orig"))
[<Fact>]
let ``extracts file name from URL with a port`` () =
Assert.Equal("test.jpg", TwitterUtils.GetImageFileName("http://example.com:80/test.jpg"))

View File

@@ -0,0 +1,49 @@
namespace Unit.Data.InjectedHTML
open Xunit
open TweetDuck.Data
module Inject =
let before = InjectedHTML.Position.Before
let after = InjectedHTML.Position.After
[<Fact>]
let ``injecting string before searched string works`` () =
Assert.Equal("<p>source[left]<br>code</p>", InjectedHTML(before, "<br>", "[left]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``injecting string after searched string works`` () =
Assert.Equal("<p>source<br>[right]code</p>", InjectedHTML(after, "<br>", "[right]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``injecting string at the beginning works`` () =
Assert.Equal("[start]<p>source<br>code</p>", InjectedHTML(before, "<p>", "[start]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``injecting string at the end works`` () =
Assert.Equal("<p>source<br>code</p>[end]", InjectedHTML(after, "</p>", "[end]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``injection only triggers for first occurrence of searched string`` () =
Assert.Equal("<p>source[left]<br>code</p><br>", InjectedHTML(before, "<br>", "[left]").InjectInto("<p>source<br>code</p><br>"))
Assert.Equal("<p>source<br>[right]code</p><br>", InjectedHTML(after, "<br>", "[right]").InjectInto("<p>source<br>code</p><br>"))
[<Fact>]
let ``empty searched string injects at the beginning`` () =
Assert.Equal("[start]<p>source<br>code</p>", InjectedHTML(before, "", "[start]").InjectInto("<p>source<br>code</p>"))
Assert.Equal("[start]<p>source<br>code</p>", InjectedHTML(after, "", "[start]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``injecting empty string does not modify source`` () =
Assert.Equal("<p>source<br>code</p>", InjectedHTML(before, "<br>", "").InjectInto("<p>source<br>code</p>"))
Assert.Equal("<p>source<br>code</p>", InjectedHTML(after, "<br>", "").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``failed match does not modify source`` () =
Assert.Equal("<p>source<br>code</p>", InjectedHTML(before, "<wrong>", "[left]").InjectInto("<p>source<br>code</p>"))
Assert.Equal("<p>source<br>code</p>", InjectedHTML(after, "<wrong>", "[right]").InjectInto("<p>source<br>code</p>"))
[<Fact>]
let ``invalid position does not modify source`` () =
Assert.Equal("<p>source<br>code</p>", InjectedHTML(enum<_>(1000), "<br>", "[somewhere]").InjectInto("<p>source<br>code</p>"))

View File

@@ -0,0 +1,57 @@
namespace Unit.Data.Result
open Xunit
open TweetDuck.Data
open System
module Result_WithValue =
let result = Result<int>(10)
[<Fact>]
let ``HasValue returns true`` () =
Assert.True(result.HasValue)
[<Fact>]
let ``accessing Value returns the provided value`` () =
Assert.Equal(10, result.Value)
[<Fact>]
let ``accessing Exception throws`` () =
Assert.Throws<InvalidOperationException>(fun () -> result.Exception |> ignore)
[<Fact>]
let ``Handle calls the correct callback`` () =
let passTest = fun _ -> ()
let failTest = fun _ -> Assert.True(false)
result.Handle(Action<_>(passTest), Action<_>(failTest))
[<Fact>]
let ``Select returns another valid Result`` () =
Assert.Equal(20, result.Select(fun x -> x * 2).Value)
module Result_WithException =
let result = Result<int>(IndexOutOfRangeException("bad"))
[<Fact>]
let ``HasValue returns false`` () =
Assert.False(result.HasValue)
[<Fact>]
let ``accessing Value throws`` () =
Assert.Throws<InvalidOperationException>(fun () -> result.Value |> ignore)
[<Fact>]
let ``accessing Exception returns the provided exception`` () =
Assert.IsType<IndexOutOfRangeException>(result.Exception)
[<Fact>]
let ``Handle calls the correct callback`` () =
let passTest = fun _ -> ()
let failTest = fun _ -> Assert.True(false)
result.Handle(Action<_>(failTest), Action<_>(passTest))
[<Fact>]
let ``Select returns a Result with the same Exception`` () =
Assert.Same(result.Exception, result.Select(fun x -> x * 2).Exception)

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.props" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.props')" />
<Import Project="..\..\packages\xunit.core.2.3.1\build\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.3.1\build\xunit.core.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>eee1071a-28fa-48b1-82a1-9cbdc5c3f2c3</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>TweetTest.Unit</RootNamespace>
<AssemblyName>TweetTest.Unit</AssemblyName>
<UseStandardResourceNames>True</UseStandardResourceNames>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFSharpCoreVersion>4.4.3.0</TargetFSharpCoreVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Name>TweetTest.Unit</Name>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup>
<MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<Tailcalls>false</Tailcalls>
<OutputPath>bin\$(Configuration)\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<WarningLevel>3</WarningLevel>
<DocumentationFile>
</DocumentationFile>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '11.0'">
<PropertyGroup Condition=" '$(FSharpTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets') ">
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets</FSharpTargetsPath>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup Condition=" '$(FSharpTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets') ">
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets</FSharpTargetsPath>
</PropertyGroup>
</Otherwise>
</Choose>
<Import Project="$(FSharpTargetsPath)" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\xunit.core.2.3.1\build\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.3.1\build\xunit.core.props'))" />
<Error Condition="!Exists('..\..\packages\xunit.core.2.3.1\build\xunit.core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.3.1\build\xunit.core.targets'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.props'))" />
<Error Condition="!Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.targets'))" />
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.3.1\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<Import Project="..\..\packages\xunit.core.2.3.1\build\xunit.core.targets" Condition="Exists('..\..\packages\xunit.core.2.3.1\build\xunit.core.targets')" />
<Import Project="..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.targets" Condition="Exists('..\..\packages\Microsoft.NET.Test.Sdk.15.7.2\build\net45\Microsoft.Net.Test.Sdk.targets')" />
<ItemGroup>
<Content Include="packages.config" />
<Compile Include="Core\TestBrowserUtils.fs" />
<Compile Include="Core\TestStringUtils.fs" />
<Compile Include="Core\TestTwitterUtils.fs" />
<Compile Include="Data\TestInjectedHTML.fs" />
<Compile Include="Data\TestResult.fs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.CodeCoverage.Shim">
<HintPath>..\..\packages\Microsoft.CodeCoverage.1.0.3\lib\netstandard1.0\Microsoft.VisualStudio.CodeCoverage.Shim.dll</HintPath>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="FSharp.Core">
<Name>FSharp.Core</Name>
<AssemblyName>FSharp.Core.dll</AssemblyName>
<HintPath>$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\$(TargetFSharpCoreVersion)\FSharp.Core.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="xunit.abstractions">
<HintPath>..\..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.assert">
<HintPath>..\..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll</HintPath>
</Reference>
<Reference Include="xunit.core">
<HintPath>..\..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll</HintPath>
</Reference>
<Reference Include="xunit.execution.desktop">
<HintPath>..\..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll</HintPath>
</Reference>
<ProjectReference Include="..\..\TweetDuck.csproj">
<Name>TweetDuck</Name>
<Project>{2389a7cd-e0d3-4706-8294-092929a33a2d}</Project>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.CodeCoverage" version="1.0.3" targetFramework="net452" />
<package id="Microsoft.NET.Test.Sdk" version="15.7.2" targetFramework="net452" />
<package id="xunit" version="2.3.1" targetFramework="net452" />
<package id="xunit.abstractions" version="2.0.1" targetFramework="net452" />
<package id="xunit.analyzers" version="0.9.0" targetFramework="net452" />
<package id="xunit.assert" version="2.3.1" targetFramework="net452" />
<package id="xunit.core" version="2.3.1" targetFramework="net452" />
<package id="xunit.extensibility.core" version="2.3.1" targetFramework="net452" />
<package id="xunit.extensibility.execution" version="2.3.1" targetFramework="net452" />
<package id="xunit.runner.visualstudio" version="2.3.1" targetFramework="net452" developmentDependency="true" />
</packages>

View File

@@ -1,53 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Core.Utils;
namespace UnitTests.Core{
[TestClass]
public class TestBrowserUtils{
[TestMethod]
public void TestIsValidUrl(){
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com")); // base
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://www.google.com")); // www.
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.co.uk")); // co.uk
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("https://google.com")); // https
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("ftp://google.com")); // ftp
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("mailto:someone@google.com")); // mailto
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/")); // trailing slash
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?")); // trailing question mark
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?a=5&b=x")); // parameters
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/#hash")); // parameters + hash
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?a=5&b=x#hash")); // parameters + hash
foreach(string tld in new string[]{ "accountants", "blackfriday", "cancerresearch", "coffee", "cool", "foo", "travelersinsurance" }){
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://test."+tld)); // long and unusual TLDs
}
Assert.AreEqual(BrowserUtils.UrlCheckResult.Tracking, BrowserUtils.CheckUrl("http://t.co/abc")); // tracking
Assert.AreEqual(BrowserUtils.UrlCheckResult.Tracking, BrowserUtils.CheckUrl("https://t.co/abc")); // tracking
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("explorer")); // file
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("explorer.exe")); // file
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("://explorer.exe")); // file-sorta
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("file://explorer.exe")); // file-proper
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("")); // empty
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("lol")); // random
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("gopher://nobody.cares")); // lmao rekt
}
[TestMethod]
public void TestGetFileNameFromUrl(){
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html"));
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html?"));
Assert.AreEqual("index.html", BrowserUtils.GetFileNameFromUrl("http://test.com/index.html?param1=abc&param2=false"));
Assert.AreEqual("index", BrowserUtils.GetFileNameFromUrl("http://test.com/index"));
Assert.AreEqual("index.", BrowserUtils.GetFileNameFromUrl("http://test.com/index."));
Assert.IsNull(BrowserUtils.GetFileNameFromUrl("http://test.com/"));
}
}
}

View File

@@ -1,36 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Core.Utils;
namespace UnitTests.Core{
[TestClass]
public class TestStringUtils{
[TestMethod]
public void TestExtractBefore(){
Assert.AreEqual("missing", StringUtils.ExtractBefore("missing", '_'));
Assert.AreEqual("", StringUtils.ExtractBefore("_empty", '_'));
Assert.AreEqual("some", StringUtils.ExtractBefore("some_text", '_'));
Assert.AreEqual("first", StringUtils.ExtractBefore("first_separator_only", '_'));
Assert.AreEqual("start_index", StringUtils.ExtractBefore("start_index_test", '_', 8));
}
[TestMethod]
public void TestParseInts(){
CollectionAssert.AreEqual(new int[0], StringUtils.ParseInts("", ','));
CollectionAssert.AreEqual(new int[]{ 1 }, StringUtils.ParseInts("1", ','));
CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts("1,2,3", ','));
CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts("1,2,3,", ','));
CollectionAssert.AreEqual(new int[]{ 1, 2, 3 }, StringUtils.ParseInts(",1,2,,3,", ','));
CollectionAssert.AreEqual(new int[]{ -50, 50 }, StringUtils.ParseInts("-50,50", ','));
}
[TestMethod]
public void TestConvertPascalCaseToScreamingSnakeCase(){
Assert.AreEqual("HELP", StringUtils.ConvertPascalCaseToScreamingSnakeCase("Help"));
Assert.AreEqual("HELP_ME", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMe"));
Assert.AreEqual("HELP_ME_PLEASE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HelpMePlease"));
Assert.AreEqual("HTML_CODE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("HTMLCode"));
Assert.AreEqual("CHECK_OUT_MY_HTML_CODE", StringUtils.ConvertPascalCaseToScreamingSnakeCase("CheckOutMyHTMLCode"));
}
}
}

View File

@@ -1,57 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Core.Utils;
namespace UnitTests.Core{
[TestClass]
public class TestTwitterUtils{
[TestMethod]
public void TestAccountRegex(){
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("http://twitter.com/chylexmc"));
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/chylexmc"));
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("http://twitter.com/chylexmc/"));
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/chylexmc/"));
Assert.AreEqual("chylexmc", TwitterUtils.RegexAccount.Match("http://twitter.com/chylexmc").Groups[1].Value);
Assert.AreEqual("123", TwitterUtils.RegexAccount.Match("http://twitter.com/123").Groups[1].Value);
Assert.AreEqual("_", TwitterUtils.RegexAccount.Match("http://twitter.com/_").Groups[1].Value);
Assert.AreEqual("Abc_123", TwitterUtils.RegexAccount.Match("http://twitter.com/Abc_123").Groups[1].Value);
Assert.AreEqual("Abc_123", TwitterUtils.RegexAccount.Match("https://twitter.com/Abc_123/").Groups[1].Value);
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("http://twitter.com/"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("http://twitter.com/chylexmc/status"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("http://nottwitter.com/chylexmc"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/chylexmc?"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("www.twitter.com/chylexmc"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/signup"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/tos"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/privacy"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/search"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/search?query"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/search-home"));
Assert.IsFalse(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/search-advanced"));
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/tosser"));
Assert.IsTrue(TwitterUtils.RegexAccount.IsMatch("https://twitter.com/searching"));
}
[TestMethod]
public void TestImageQualityLink(){
Assert.AreEqual("https://pbs.twimg.com/profile_images/123", TwitterUtils.GetMediaLink("https://pbs.twimg.com/profile_images/123", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123", TwitterUtils.GetMediaLink("https://pbs.twimg.com/profile_images/123", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.GetMediaLink("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.GetMediaLink("https://pbs.twimg.com/profile_images/123.jpg", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:orig", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:orig", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg:small", TwitterUtils.ImageQuality.Orig));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:large", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg:large", TwitterUtils.ImageQuality.Default));
Assert.AreEqual("https://pbs.twimg.com/media/123.jpg:orig", TwitterUtils.GetMediaLink("https://pbs.twimg.com/media/123.jpg:large", TwitterUtils.ImageQuality.Orig));
}
}
}

View File

@@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Data;
namespace UnitTests.Data{
[TestClass]
public class TestInjectedHTML{
private static IEnumerable<InjectedHTML.Position> Positions => Enum.GetValues(typeof(InjectedHTML.Position)).Cast<InjectedHTML.Position>();
[TestMethod]
public void TestFailedMatches(){
foreach(var pos in Positions){
Assert.AreEqual(string.Empty, new InjectedHTML(pos, "b", "b").Inject(string.Empty));
Assert.AreEqual("aaaa", new InjectedHTML(pos, "b", "b").Inject("aaaa"));
}
}
[TestMethod]
public void TestEmptySearch(){
foreach(var pos in Positions){
Assert.AreEqual("b", new InjectedHTML(pos, string.Empty, "b").Inject(string.Empty));
Assert.AreEqual("baaaa", new InjectedHTML(pos, string.Empty, "b").Inject("aaaa"));
}
}
[TestMethod]
public void TestEmptyHTML(){
foreach(var pos in Positions){
Assert.AreEqual(string.Empty, new InjectedHTML(pos, string.Empty, string.Empty).Inject(string.Empty));
Assert.AreEqual("aaaa", new InjectedHTML(pos, string.Empty, string.Empty).Inject("aaaa"));
Assert.AreEqual("aaaa", new InjectedHTML(pos, "a", string.Empty).Inject("aaaa"));
Assert.AreEqual("aaaa", new InjectedHTML(pos, "b", string.Empty).Inject("aaaa"));
}
}
[TestMethod]
public void TestInvalidPosition(){
Assert.AreEqual("aaaa", new InjectedHTML((InjectedHTML.Position)(Positions.Count()+1), "a", "b").Inject("aaaa"));
}
[TestMethod]
public void TestPositions(){
Assert.AreEqual("aaabcxaaa", new InjectedHTML(InjectedHTML.Position.Before, "x", "bc").Inject("aaaxaaa"));
Assert.AreEqual("aaaxbcaaa", new InjectedHTML(InjectedHTML.Position.After, "x", "bc").Inject("aaaxaaa"));
Assert.AreEqual("bcxaaa", new InjectedHTML(InjectedHTML.Position.Before, "x", "bc").Inject("xaaa"));
Assert.AreEqual("xbcaaa", new InjectedHTML(InjectedHTML.Position.After, "x", "bc").Inject("xaaa"));
Assert.AreEqual("aaabcx", new InjectedHTML(InjectedHTML.Position.Before, "x", "bc").Inject("aaax"));
Assert.AreEqual("aaaxbc", new InjectedHTML(InjectedHTML.Position.After, "x", "bc").Inject("aaax"));
}
[TestMethod]
public void TestFirstOccurrence(){
Assert.AreEqual("bcaaaa", new InjectedHTML(InjectedHTML.Position.Before, "a", "bc").Inject("aaaa"));
Assert.AreEqual("abcaaa", new InjectedHTML(InjectedHTML.Position.After, "a", "bc").Inject("aaaa"));
Assert.AreEqual("bcaaaa", new InjectedHTML(InjectedHTML.Position.Before, "aa", "bc").Inject("aaaa"));
Assert.AreEqual("aabcaa", new InjectedHTML(InjectedHTML.Position.After, "aa", "bc").Inject("aaaa"));
}
}
}

View File

@@ -24,11 +24,6 @@
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core">
@@ -49,13 +44,9 @@
</Choose>
<ItemGroup>
<Compile Include="Configuration\TestUserConfig.cs" />
<Compile Include="Core\TestStringUtils.cs" />
<Compile Include="Core\TestTwitterUtils.cs" />
<Compile Include="Data\TestCombinedFileStream.cs" />
<Compile Include="Core\TestBrowserUtils.cs" />
<Compile Include="Data\TestCommandLineArgs.cs" />
<Compile Include="Data\TestFileSerializer.cs" />
<Compile Include="Data\TestInjectedHTML.cs" />
<Compile Include="Data\TestTwoKeyDictionary.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestIO.cs" />