Skip to content
Merged
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
82 changes: 82 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
build-and-test:
name: Build & test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: ${{ runner.os }}-nuget-

- name: Restore
run: dotnet restore KursKayitSistemi.slnx

- name: Build
run: dotnet build KursKayitSistemi.slnx --no-restore --configuration Release

- name: Test
run: >
dotnet test KursKayitSistemi.slnx
--no-build
--configuration Release
--logger "trx;LogFileName=test-results.trx"
--collect:"XPlat Code Coverage"
--results-directory ./TestResults

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: ./TestResults
retention-days: 7

vulnerable-packages:
name: Dependency audit
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore KursKayitSistemi.slnx

# Transitive dependencies are included: the SQLite native bundle reached this project
# through EF Core, not through anything declared here.
- name: Fail on known vulnerable packages
run: |
dotnet list KursKayitSistemi.slnx package --vulnerable --include-transitive 2>&1 | tee audit.log
if grep -q -E '(Yüksek|High|Critical|Moderate)' audit.log; then
echo "::error::Bilinen güvenlik açığı olan paket bulundu."
exit 1
fi
echo "Açık bulunamadı."
39 changes: 38 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
## Visual Studio / .NET.vs/bin/obj/*.user*.suo*.userprefs[Dd]ebug/[Rr]elease/[Bb]uild[Ll]og.*## Build results*.dll*.exe*.pdb*.cache## NuGetpackages/*.nupkg## Local database files (generated by EF migrations)*.db*.db-shm*.db-wal*.sqlite*.sqlite3## Environment / secretsappsettings.*.local.json.env
## Visual Studio / .NET
.vs/
bin/
obj/
*.user
*.suo
*.userprefs
[Dd]ebug/
[Rr]elease/
[Bb]uild[Ll]og.*

## Build results
*.dll
*.exe
*.pdb
*.cache

## Test results
[Tt]est[Rr]esult*/
coverage/
*.trx
*.coverage
*.cobertura.xml

## NuGet
packages/
*.nupkg

## Local database files (generated by EF migrations)
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3

## Environment / secrets
appsettings.*.local.json
.env
176 changes: 176 additions & 0 deletions KursKayitSistemi.Tests/AccountServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using KursKayitSistemi.Models;
using KursKayitSistemi.Services;
using KursKayitSistemi.Tests.Infrastructure;
using Microsoft.EntityFrameworkCore;

namespace KursKayitSistemi.Tests;

public sealed class AccountServiceTests : IDisposable
{
private readonly SqliteTestDatabase _db = new();
private readonly IPasswordHashService _hasher = new Pbkdf2PasswordHashService(iterations: 1_000);

public void Dispose() => _db.Dispose();

private AccountService NewService(AppDbContext ctx) => new(ctx, _hasher);

// ----------------------------------------------------------------- registration

[Fact]
public async Task Kayit_sifreyi_hashleyerek_saklar()
{
await using (var ctx = _db.CreateContext())
{
var result = await NewService(ctx).RegisterAsync("1001", "Ayşe Yılmaz", "ayse@ornek.test", "GucluSifre123");
Assert.True(result.Success);
}

await using (var ctx = _db.CreateContext())
{
var ogrenci = await ctx.Ogrenciler.SingleAsync();

Assert.NotEqual("GucluSifre123", ogrenci.SifreHash);
Assert.StartsWith("pbkdf2-sha256$", ogrenci.SifreHash);
Assert.True(_hasher.Verify("GucluSifre123", ogrenci.SifreHash));
}
}

[Fact]
public async Task Ayni_ogrenci_numarasi_ikinci_kez_kaydedilemez()
{
await using (var ctx = _db.CreateContext())
await NewService(ctx).RegisterAsync("1001", "Ayşe", "ayse@ornek.test", "GucluSifre123");

await using (var ctx = _db.CreateContext())
{
var result = await NewService(ctx).RegisterAsync("1001", "Başka Kişi", "baska@ornek.test", "GucluSifre123");

Assert.False(result.Success);
Assert.Equal(RegistrationError.DuplicateOgrenciNo, result.Error);
}
}

[Fact]
public async Task Ayni_email_ikinci_kez_kaydedilemez()
{
await using (var ctx = _db.CreateContext())
await NewService(ctx).RegisterAsync("1001", "Ayşe", "ayse@ornek.test", "GucluSifre123");

await using (var ctx = _db.CreateContext())
{
var result = await NewService(ctx).RegisterAsync("1002", "Başka Kişi", "ayse@ornek.test", "GucluSifre123");

Assert.False(result.Success);
Assert.Equal(RegistrationError.DuplicateEmail, result.Error);
}
}

[Fact]
public async Task Kayit_bosluklari_temizler()
{
await using (var ctx = _db.CreateContext())
Assert.True((await NewService(ctx).RegisterAsync(" 1001 ", " Ayşe Yılmaz ", " ayse@ornek.test ", "GucluSifre123")).Success);

await using (var ctx = _db.CreateContext())
{
var ogrenci = await ctx.Ogrenciler.SingleAsync();
Assert.Equal("1001", ogrenci.OgrenciNo);
Assert.Equal("Ayşe Yılmaz", ogrenci.AdSoyad);
Assert.Equal("ayse@ornek.test", ogrenci.Email);
}
}

// --------------------------------------------------------------- authentication

[Fact]
public async Task Dogru_bilgilerle_ogrenci_girisi_basarili()
{
await using (var ctx = _db.CreateContext())
await NewService(ctx).RegisterAsync("1001", "Ayşe Yılmaz", "ayse@ornek.test", "GucluSifre123");

await using (var ctx = _db.CreateContext())
{
var user = await NewService(ctx).AuthenticateAsync("1001", "GucluSifre123");

Assert.NotNull(user);
Assert.Equal(Roller.Ogrenci, user.Role);
Assert.Equal("Ayşe Yılmaz", user.AdSoyad);
Assert.NotEqual(0, user.Id);
}
}

[Fact]
public async Task Yanlis_sifreyle_giris_reddedilir()
{
await using (var ctx = _db.CreateContext())
await NewService(ctx).RegisterAsync("1001", "Ayşe", "ayse@ornek.test", "GucluSifre123");

await using (var ctx = _db.CreateContext())
Assert.Null(await NewService(ctx).AuthenticateAsync("1001", "YanlisSifre"));
}

[Fact]
public async Task Olmayan_kullanici_icin_giris_reddedilir()
{
await using var ctx = _db.CreateContext();
Assert.Null(await NewService(ctx).AuthenticateAsync("boyle-biri-yok", "herhangi"));
}

[Theory]
[InlineData("", "sifre")]
[InlineData(" ", "sifre")]
[InlineData("1001", "")]
public async Task Bos_kimlik_bilgileri_reddedilir(string kullaniciAdi, string sifre)
{
await using var ctx = _db.CreateContext();
Assert.Null(await NewService(ctx).AuthenticateAsync(kullaniciAdi, sifre));
}

/// <summary>
/// The administrator used to be a pair of string literals in the login action, which meant
/// the working credentials were published with the source. Now it is a row like any other.
/// </summary>
[Fact]
public async Task Yonetici_girisi_veritabanindaki_hash_uzerinden_dogrulanir()
{
await using (var ctx = _db.CreateContext())
{
ctx.Yoneticiler.Add(new Yonetici
{
KullaniciAdi = "admin",
AdSoyad = "Sistem Yöneticisi",
SifreHash = _hasher.Hash("BuSifreKaynakKodundaDegil")
});
await ctx.SaveChangesAsync();
}

await using (var ctx = _db.CreateContext())
{
var user = await NewService(ctx).AuthenticateAsync("admin", "BuSifreKaynakKodundaDegil");

Assert.NotNull(user);
Assert.Equal(Roller.Admin, user.Role);
}

// The credentials that used to be hard-coded must no longer work.
await using (var ctx = _db.CreateContext())
Assert.Null(await NewService(ctx).AuthenticateAsync("admin", "1234"));
}

[Fact]
public async Task Ogrenci_admin_rolu_alamaz()
{
await using (var ctx = _db.CreateContext())
await NewService(ctx).RegisterAsync("1001", "Sistem Yöneticisi", "sahte@ornek.test", "GucluSifre123");

await using (var ctx = _db.CreateContext())
{
// Registering under the administrator's display name grants nothing: the role
// comes from which table the row lives in, not from what the name says.
var user = await NewService(ctx).AuthenticateAsync("1001", "GucluSifre123");

Assert.NotNull(user);
Assert.Equal(Roller.Ogrenci, user.Role);
}
}
}
Loading
Loading