Skip to content

Commit 8775361

Browse files
committed
Initial commit
1 parent af42b6a commit 8775361

40 files changed

Lines changed: 2263 additions & 0 deletions

BUILD.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Build Commands
2+
3+
## Development Build
4+
```bash
5+
dotnet build
6+
```
7+
8+
## Self-Contained Release Builds
9+
```bash
10+
# Windows x64 (single executable, no dependencies required)
11+
dotnet publish -r win-x64 -c Release
12+
13+
# Linux x64 (single executable, no dependencies required)
14+
dotnet publish -r linux-x64 -c Release
15+
16+
# macOS x64 (single executable, no dependencies required)
17+
dotnet publish -r osx-x64 -c Release
18+
19+
# macOS ARM64 (Apple Silicon)
20+
dotnet publish -r osx-arm64 -c Release
21+
```
22+
23+
Output locations:
24+
- `bin/Release/net8.0/{runtime}/publish/QueryPush.exe` (Windows)
25+
- `bin/Release/net8.0/{runtime}/publish/QueryPush` (Linux/macOS)
26+
27+
## Project Configuration
28+
29+
The following properties ensure single-file, self-contained deployment:
30+
31+
- `PublishSingleFile=true` - Bundles into single executable
32+
- `SelfContained=true` - Includes .NET runtime
33+
- `PublishTrimmed=true` - Removes unused code (smaller size)
34+
- `Version=1.0.0` - Assembly version metadata
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using FluentAssertions;
2+
using QueryPush.Configuration;
3+
using QueryPush.Services;
4+
using Xunit;
5+
using Xunit.Categories;
6+
7+
namespace QueryPush.Tests;
8+
9+
public class ConfigurationValidatorTests
10+
{
11+
[Fact, UnitTest]
12+
public void ValidateConfiguration_WithMissingDatabase_ShouldThrow()
13+
{
14+
var settings = new QueryPushSettings
15+
{
16+
Databases = [],
17+
Endpoints = [new EndpointConfig { Name = "test", Url = "http://test" }],
18+
Queries = []
19+
};
20+
21+
var monitor = new TestOptionsMonitor<QueryPushSettings>(settings);
22+
var validator = new ConfigurationValidator(monitor);
23+
24+
var act = () => validator.ValidateConfiguration();
25+
26+
act.Should().Throw<System.ComponentModel.DataAnnotations.ValidationException>();
27+
}
28+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using FluentAssertions;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
5+
using QueryPush.Configuration;
6+
using QueryPush.Services;
7+
using Xunit;
8+
using Xunit.Categories;
9+
10+
namespace QueryPush.Tests;
11+
12+
public class DatabaseIntegrationTests : IDisposable
13+
{
14+
private readonly IServiceProvider _services;
15+
private readonly string _testDbPath = "integration_test.db";
16+
17+
public DatabaseIntegrationTests()
18+
{
19+
var configuration = new ConfigurationBuilder()
20+
.AddInMemoryCollection(new Dictionary<string, string?>
21+
{
22+
["databases:0:name"] = "TestDb",
23+
["databases:0:connectionString"] = $"Driver={{SQLite3 ODBC Driver}};Database={_testDbPath};",
24+
["endpoints:0:name"] = "TestEndpoint",
25+
["endpoints:0:url"] = "https://webhook.site/test"
26+
})
27+
.Build();
28+
29+
var services = new ServiceCollection();
30+
services.Configure<QueryPushSettings>(configuration);
31+
services.AddLogging(builder => builder.ClearProviders());
32+
services.AddScoped<IDatabaseService, DatabaseService>();
33+
34+
_services = services.BuildServiceProvider();
35+
}
36+
37+
[Fact, IntegrationTest]
38+
public async Task DatabaseService_WithSQLiteODBC_ShouldCreateTableAndQuery()
39+
{
40+
var dbService = _services.GetRequiredService<IDatabaseService>();
41+
42+
await dbService.ExecuteQueryAsync("TestDb",
43+
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)", 30, 1000);
44+
45+
await dbService.ExecuteQueryAsync("TestDb",
46+
"INSERT OR REPLACE INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com'), (2, 'Jane Smith', 'jane@example.com')", 30, 1000);
47+
48+
var results = await dbService.ExecuteQueryAsync("TestDb",
49+
"SELECT id, name, email FROM users ORDER BY id", 30, 1000);
50+
51+
results.Should().HaveCount(2);
52+
results.First()["name"].Should().Be("John Doe");
53+
results.Last()["email"].Should().Be("jane@example.com");
54+
}
55+
56+
[Fact, IntegrationTest]
57+
public async Task DatabaseService_WithInvalidQuery_ShouldThrow()
58+
{
59+
var dbService = _services.GetRequiredService<IDatabaseService>();
60+
61+
var act = async () => await dbService.ExecuteQueryAsync("TestDb", "SELECT * FROM nonexistent_table", 30, 100);
62+
63+
await act.Should().ThrowAsync<Exception>();
64+
}
65+
66+
public void Dispose()
67+
{
68+
if (File.Exists(_testDbPath))
69+
File.Delete(_testDbPath);
70+
}
71+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using FluentAssertions;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
5+
using QueryPush.Configuration;
6+
using QueryPush.Services;
7+
using Xunit;
8+
using Xunit.Categories;
9+
10+
namespace QueryPush.Tests;
11+
12+
public class DatabaseServiceTests : IDisposable
13+
{
14+
private readonly IServiceProvider _services;
15+
private readonly string _testDbPath = "database_test.db";
16+
17+
public DatabaseServiceTests()
18+
{
19+
var configuration = new ConfigurationBuilder()
20+
.AddInMemoryCollection(new Dictionary<string, string?>
21+
{
22+
["databases:0:name"] = "TestDb",
23+
["databases:0:connectionString"] = $"Driver={{SQLite3 ODBC Driver}};Database={_testDbPath};",
24+
["endpoints:0:name"] = "TestEndpoint",
25+
["endpoints:0:url"] = "https://webhook.site/test"
26+
})
27+
.Build();
28+
29+
var services = new ServiceCollection();
30+
services.Configure<QueryPushSettings>(configuration);
31+
services.AddLogging(builder => builder.ClearProviders());
32+
services.AddScoped<IDatabaseService, DatabaseService>();
33+
34+
_services = services.BuildServiceProvider();
35+
}
36+
37+
[Fact, IntegrationTest]
38+
public async Task ExecuteQueryAsync_WithValidSqliteQuery_ShouldReturnResults()
39+
{
40+
var service = _services.GetRequiredService<IDatabaseService>();
41+
42+
await service.ExecuteQueryAsync("TestDb", "CREATE TABLE IF NOT EXISTS temp_test (id INTEGER, name TEXT)", 30, 100);
43+
await service.ExecuteQueryAsync("TestDb", "INSERT OR REPLACE INTO temp_test VALUES (1, 'Test')", 30, 100);
44+
45+
var results = await service.ExecuteQueryAsync("TestDb", "SELECT * FROM temp_test", 30, 100);
46+
47+
results.Should().HaveCount(1);
48+
results.First()["id"].Should().Be(1);
49+
results.First()["name"].Should().Be("Test");
50+
}
51+
52+
[Fact, IntegrationTest]
53+
public async Task ExecuteQueryAsync_WithInvalidDatabase_ShouldThrow()
54+
{
55+
var service = _services.GetRequiredService<IDatabaseService>();
56+
57+
var act = async () => await service.ExecuteQueryAsync("NonExistent", "SELECT 1", 30, 100);
58+
59+
await act.Should().ThrowAsync<InvalidOperationException>();
60+
}
61+
62+
public void Dispose()
63+
{
64+
if (File.Exists(_testDbPath))
65+
File.Delete(_testDbPath);
66+
}
67+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.3.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
15+
<PackageReference Include="xunit" Version="2.6.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
17+
<PackageReference Include="Xunit.Categories" Version="2.0.8" />
18+
<PackageReference Include="FluentAssertions" Version="6.12.0" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\QueryPush\QueryPush.csproj" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<None Update="appsettings.json">
27+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
28+
</None>
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.Extensions.Options;
2+
using Microsoft.Extensions.Primitives;
3+
4+
namespace QueryPush.Tests;
5+
6+
public class TestOptionsMonitor<T> : IOptionsMonitor<T>
7+
{
8+
public TestOptionsMonitor(T currentValue)
9+
{
10+
CurrentValue = currentValue;
11+
}
12+
13+
public T CurrentValue { get; }
14+
15+
public T Get(string? name) => CurrentValue;
16+
17+
public IDisposable OnChange(Action<T, string> listener) => new TestDisposable();
18+
19+
private class TestDisposable : IDisposable
20+
{
21+
public void Dispose() { }
22+
}
23+
}

QueryPush.Tests/appsettings.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"databases": [
3+
{
4+
"name": "TestDb",
5+
"connectionString": "Driver={SQLite3 ODBC Driver};Database=test.db;"
6+
}
7+
],
8+
"endpoints": [
9+
{
10+
"name": "TestEndpoint",
11+
"url": "https://webhook.site/61d18057-d3c2-4011-9abe-1dc8ec56961d",
12+
"retryAttempts": 1,
13+
"requestDelay": 0
14+
}
15+
],
16+
"queries": [
17+
{
18+
"name": "TestQuery",
19+
"cron": "0 0 0 * * ?",
20+
"database": "TestDb",
21+
"endpoint": "TestEndpoint",
22+
"enabled": false,
23+
"queryText": "SELECT 1 as TestValue"
24+
}
25+
]
26+
}

QueryPush.sln

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.13.35828.75
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryPush", "QueryPush\QueryPush.csproj", "{A37F8B03-5F67-3338-B39E-11104E87FCC7}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryPush.Tests", "QueryPush.Tests\QueryPush.Tests.csproj", "{92A8BC83-8418-178D-A1CE-30D25523D8C1}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{A37F8B03-5F67-3338-B39E-11104E87FCC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{A37F8B03-5F67-3338-B39E-11104E87FCC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{A37F8B03-5F67-3338-B39E-11104E87FCC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{A37F8B03-5F67-3338-B39E-11104E87FCC7}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{92A8BC83-8418-178D-A1CE-30D25523D8C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{92A8BC83-8418-178D-A1CE-30D25523D8C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{92A8BC83-8418-178D-A1CE-30D25523D8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{92A8BC83-8418-178D-A1CE-30D25523D8C1}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {937DF097-0E8A-4FB8-93BF-DCC5EF908D88}
30+
EndGlobalSection
31+
EndGlobal

QueryPush.sln.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace QueryPush.Configuration;
2+
3+
public class AlertConfig
4+
{
5+
public SlackAlertConfig? Slack { get; set; }
6+
public EmailAlertConfig? Email { get; set; }
7+
}

0 commit comments

Comments
 (0)