235 lines
8.4 KiB
C#
235 lines
8.4 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using Nonemm.Core;
|
|
|
|
namespace Nonemm.Storage.Tests;
|
|
|
|
public class SqliteLogStoreTests : IDisposable
|
|
{
|
|
private readonly string path = Path.Combine(Path.GetTempPath(), $"nonemm-{Guid.NewGuid():N}.s3db");
|
|
|
|
public void Dispose()
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
File.Delete(path);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private static Qso Contact(string call, DateTime at, int contestNumber = 1) => new()
|
|
{
|
|
Id = Qso.NewId(),
|
|
TimestampUtc = at,
|
|
Call = Callsign.Parse(call),
|
|
Frequency = Frequency.FromKilohertz(14_025.5),
|
|
Mode = Modes.Cw,
|
|
ContestName = "CQWW",
|
|
ContestNumber = contestNumber,
|
|
SentReport = "599",
|
|
ReceivedReport = "599",
|
|
Zone = 14,
|
|
Points = 3,
|
|
IsMultiplier1 = true,
|
|
CountryPrefix = "DL",
|
|
Continent = "EU",
|
|
WpxPrefix = "DL1",
|
|
};
|
|
|
|
[Fact]
|
|
public void ContactSurvivesARoundTrip()
|
|
{
|
|
DateTime at = new(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc);
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
Qso written = store.Add(Contact("DL1ABC", at));
|
|
|
|
Qso read = store.Qsos(1).Single();
|
|
Assert.Equal(written.Id, read.Id);
|
|
Assert.Equal("DL1ABC", read.Call.Text);
|
|
Assert.Equal(at, read.TimestampUtc);
|
|
Assert.Equal(14_025_500, read.Frequency.Hertz);
|
|
Assert.Equal(Modes.Cw, read.Mode);
|
|
Assert.Equal(14, read.Zone);
|
|
Assert.Equal("EU", read.Continent);
|
|
}
|
|
|
|
[Fact]
|
|
public void SecondContactInTheSameSecondMovesOnASecond()
|
|
{
|
|
DateTime at = new(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc);
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
store.Add(Contact("DL1ABC", at));
|
|
Qso second = store.Add(Contact("DL1ABC", at));
|
|
Assert.Equal(at.AddSeconds(1), second.TimestampUtc);
|
|
Assert.Equal(2, store.Qsos(1).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void EditedContactKeepsItsIdentity()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
Qso written = store.Add(Contact("DL1ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc)));
|
|
store.Update(written with { ReceivedReport = "579", Zone = 15 });
|
|
|
|
Qso read = store.Qsos(1).Single();
|
|
Assert.Equal(written.Id, read.Id);
|
|
Assert.Equal("579", read.ReceivedReport);
|
|
Assert.Equal(15, read.Zone);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeletedContactIsGone()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
Qso written = store.Add(Contact("DL1ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc)));
|
|
store.Delete(written.Id);
|
|
Assert.Empty(store.Qsos(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void ContestsAreNumberedInTurn()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
ContestInstance first = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWW" });
|
|
ContestInstance second = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWPX" });
|
|
Assert.Equal(1, first.ContestNumber);
|
|
Assert.Equal(2, second.ContestNumber);
|
|
Assert.Equal(2, store.Contests().Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContactsBelongToTheirOwnContest()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
store.Add(Contact("DL1ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc), contestNumber: 1));
|
|
store.Add(Contact("DL2ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc), contestNumber: 2));
|
|
Assert.Equal("DL1ABC", store.Qsos(1).Single().Call.Text);
|
|
Assert.Equal("DL2ABC", store.Qsos(2).Single().Call.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void NewDatabaseCarriesN1mmTablesAndVersion()
|
|
{
|
|
using (SqliteLogStore store = SqliteLogStore.Open(path))
|
|
{
|
|
}
|
|
|
|
using SqliteConnection connection = new($"Data Source={path}");
|
|
connection.Open();
|
|
using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
|
using SqliteDataReader row = command.ExecuteReader();
|
|
List<string> tables = [];
|
|
while (row.Read())
|
|
{
|
|
tables.Add(row.GetString(0));
|
|
}
|
|
Assert.Contains("DXLOG", tables);
|
|
Assert.Contains("ContestInstance", tables);
|
|
Assert.Contains("Contest", tables);
|
|
Assert.Contains("Station", tables);
|
|
|
|
using SqliteCommand version = connection.CreateCommand();
|
|
version.CommandText = "PRAGMA user_version";
|
|
Assert.Equal(4L, version.ExecuteScalar());
|
|
}
|
|
|
|
[Fact]
|
|
public void ContactIdCannotBeChanged()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
store.Add(Contact("DL1ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc)));
|
|
using SqliteConnection connection = new($"Data Source={path}");
|
|
connection.Open();
|
|
using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = "UPDATE DXLOG SET ID = 'zzzz'";
|
|
Assert.Throws<SqliteException>(() => command.ExecuteNonQuery());
|
|
}
|
|
|
|
/// N1MM matches a contact to its contest on ContestNR. ContestID is only the
|
|
/// table's key, and in a log N1MM has been using for a while the two have
|
|
/// drifted apart, so keying on ContestID hands back another contest's log.
|
|
[Fact]
|
|
public void AContestIsFoundByContestNrNotByTheTableKey()
|
|
{
|
|
using (SqliteLogStore store = SqliteLogStore.Open(path))
|
|
{
|
|
Execute("""
|
|
INSERT INTO ContestInstance (ContestID, ContestName, ContestNR, ModeCategory)
|
|
VALUES (9, 'CQWWSSB', 7, 'SSB')
|
|
""");
|
|
store.Add(Contact("DL1ABC", new DateTime(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc), 7));
|
|
store.Add(Contact("JA1XYZ", new DateTime(2026, 5, 30, 12, 1, 0, DateTimeKind.Utc), 9));
|
|
}
|
|
|
|
using SqliteLogStore reopened = SqliteLogStore.Open(path);
|
|
ContestInstance? found = reopened.Contest(7);
|
|
|
|
Assert.Equal("CQWWSSB", found?.ContestName);
|
|
Assert.Equal(7, found?.ContestNumber);
|
|
Assert.Equal(["DL1ABC"], reopened.Qsos(7).Select(q => q.Call.Text));
|
|
Assert.Null(reopened.Contest(9));
|
|
}
|
|
|
|
[Fact]
|
|
public void ANewContestGetsANumberOfItsOwn()
|
|
{
|
|
using SqliteLogStore store = SqliteLogStore.Open(path);
|
|
Execute("""
|
|
INSERT INTO ContestInstance (ContestID, ContestName, ContestNR, ModeCategory)
|
|
VALUES (9, 'CQWWSSB', 7, 'SSB')
|
|
""");
|
|
|
|
ContestInstance added = store.AddContest(new ContestInstance
|
|
{
|
|
ContestNumber = 0,
|
|
ContestName = "CQWWCW",
|
|
});
|
|
|
|
Assert.Equal(8, added.ContestNumber);
|
|
Assert.Equal("CQWWCW", store.Contest(8)?.ContestName);
|
|
}
|
|
|
|
private void Execute(string sql)
|
|
{
|
|
using SqliteConnection connection = new($"Data Source={path}");
|
|
connection.Open();
|
|
using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = sql;
|
|
command.ExecuteNonQuery();
|
|
}
|
|
|
|
[Fact]
|
|
public void SavingADefinitionAgainKeepsTheMessagesN1mmStored()
|
|
{
|
|
ContestDefinition definition = new()
|
|
{
|
|
Name = "CQWWCW",
|
|
DisplayName = "CQ WorldWide CW",
|
|
CabrilloName = "CQ-WW-CW",
|
|
Mode = "CW",
|
|
};
|
|
using (SqliteLogStore store = SqliteLogStore.Open(path))
|
|
{
|
|
store.SaveContestDefinition(definition);
|
|
}
|
|
using (SqliteConnection connection = new($"Data Source={path}"))
|
|
{
|
|
connection.Open();
|
|
using SqliteCommand set = connection.CreateCommand();
|
|
set.CommandText = "UPDATE Contest SET CWMessages = 'F1;CQ' WHERE Name = 'CQWWCW'";
|
|
set.ExecuteNonQuery();
|
|
}
|
|
|
|
using (SqliteLogStore store = SqliteLogStore.Open(path))
|
|
{
|
|
store.SaveContestDefinition(definition with { DisplayName = "Renamed" });
|
|
}
|
|
|
|
using SqliteConnection check = new($"Data Source={path}");
|
|
check.Open();
|
|
using SqliteCommand read = check.CreateCommand();
|
|
read.CommandText = "SELECT DisplayName, CWMessages FROM Contest WHERE Name = 'CQWWCW'";
|
|
using SqliteDataReader row = read.ExecuteReader();
|
|
Assert.True(row.Read());
|
|
Assert.Equal(("Renamed", "F1;CQ"), (row.GetString(0), row.GetString(1)));
|
|
}
|
|
}
|