update
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class AttendanceCheckInChallengeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Challenge_IsBoundToSheetSecretAndTwentySecondLifetime()
|
||||
{
|
||||
var sheetId = Guid.NewGuid();
|
||||
var issuedAt = new DateTime(2026, 7, 28, 8, 0, 0, DateTimeKind.Utc);
|
||||
var challenge = AttendanceCheckInChallenge.Create(
|
||||
sheetId,
|
||||
"sheet-secret",
|
||||
issuedAt);
|
||||
|
||||
Assert.True(AttendanceCheckInChallenge.TryReadSheetId(
|
||||
challenge.Token,
|
||||
out var parsedSheetId));
|
||||
Assert.Equal(sheetId, parsedSheetId);
|
||||
Assert.True(AttendanceCheckInChallenge.IsValid(
|
||||
challenge.Token,
|
||||
sheetId,
|
||||
"sheet-secret",
|
||||
issuedAt.AddSeconds(20)));
|
||||
Assert.False(AttendanceCheckInChallenge.IsValid(
|
||||
challenge.Token,
|
||||
sheetId,
|
||||
"sheet-secret",
|
||||
issuedAt.AddSeconds(21)));
|
||||
Assert.False(AttendanceCheckInChallenge.IsValid(
|
||||
challenge.Token,
|
||||
Guid.NewGuid(),
|
||||
"sheet-secret",
|
||||
issuedAt));
|
||||
Assert.False(AttendanceCheckInChallenge.IsValid(
|
||||
challenge.Token,
|
||||
sheetId,
|
||||
"different-secret",
|
||||
issuedAt));
|
||||
}
|
||||
}
|
||||
@@ -55,10 +55,19 @@ public sealed class AttendanceControllerTests
|
||||
EnrollmentYear = 2026,
|
||||
EnrollmentDate = new DateOnly(2026, 9, 1)
|
||||
};
|
||||
var secondStudentUserId = Guid.NewGuid();
|
||||
var secondStudentUser = new ApplicationUser
|
||||
{
|
||||
Id = secondStudentUserId,
|
||||
UserName = "202601002",
|
||||
NormalizedUserName = "202601002",
|
||||
DisplayName = "吴同学"
|
||||
};
|
||||
var secondStudent = new Student
|
||||
{
|
||||
StudentNumber = "202601002",
|
||||
Name = "吴同学",
|
||||
UserId = secondStudentUserId,
|
||||
AdministrativeClassId = administrativeClass.Id,
|
||||
EnrollmentYear = 2026,
|
||||
EnrollmentDate = new DateOnly(2026, 9, 1)
|
||||
@@ -95,6 +104,7 @@ public sealed class AttendanceControllerTests
|
||||
};
|
||||
db.AddRange(
|
||||
studentUser,
|
||||
secondStudentUser,
|
||||
college,
|
||||
major,
|
||||
administrativeClass,
|
||||
@@ -198,6 +208,11 @@ public sealed class AttendanceControllerTests
|
||||
StudentId = firstStudent.Id,
|
||||
Status = AttendanceStatus.Absent
|
||||
};
|
||||
var secondQrRecord = new AttendanceRecord
|
||||
{
|
||||
StudentId = secondStudent.Id,
|
||||
Status = AttendanceStatus.Absent
|
||||
};
|
||||
var qrSheet = new AttendanceSheet
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
@@ -208,10 +223,17 @@ public sealed class AttendanceControllerTests
|
||||
CheckInToken = "TEST-QR-TOKEN",
|
||||
CheckInStartsAt = now.AddMinutes(-1),
|
||||
CheckInEndsAt = now.AddMinutes(10),
|
||||
Records = [qrRecord]
|
||||
Records = [qrRecord, secondQrRecord]
|
||||
};
|
||||
db.AttendanceSheets.Add(qrSheet);
|
||||
await db.SaveChangesAsync();
|
||||
var qrChallengeResult = await controller.GetQrChallenge(
|
||||
qrSheet.Id,
|
||||
CancellationToken.None);
|
||||
var qrChallengeOk = Assert.IsType<OkObjectResult>(qrChallengeResult);
|
||||
var qrToken = Assert.IsType<string>(
|
||||
qrChallengeOk.Value!.GetType().GetProperty("Token")!.GetValue(
|
||||
qrChallengeOk.Value));
|
||||
|
||||
var studentController = new AttendanceController(
|
||||
db,
|
||||
@@ -231,23 +253,55 @@ public sealed class AttendanceControllerTests
|
||||
item.GetType().GetProperty("TeachingTaskId")!.GetValue(item)));
|
||||
|
||||
var infoResult = await studentController.GetCheckInInfo(
|
||||
qrSheet.CheckInToken,
|
||||
qrToken,
|
||||
CancellationToken.None);
|
||||
Assert.IsType<OkObjectResult>(infoResult);
|
||||
Assert.IsType<NotFoundResult>(
|
||||
await studentController.GetCheckInInfo(
|
||||
qrSheet.CheckInToken,
|
||||
CancellationToken.None));
|
||||
|
||||
var qrCheckInResult = await studentController.CheckIn(
|
||||
new AttendanceCheckInRequest(
|
||||
null,
|
||||
qrSheet.CheckInToken,
|
||||
qrToken,
|
||||
null,
|
||||
null,
|
||||
null),
|
||||
null,
|
||||
"test-device-1",
|
||||
"android"),
|
||||
CancellationToken.None);
|
||||
Assert.IsType<OkObjectResult>(qrCheckInResult);
|
||||
Assert.Equal(AttendanceStatus.Present, qrRecord.Status);
|
||||
Assert.Equal(AttendanceCheckInMethod.QrCode, qrRecord.CheckedInMethod);
|
||||
Assert.NotNull(qrRecord.CheckInAt);
|
||||
Assert.Null(qrRecord.CheckInLatitude);
|
||||
var qrAttempt = await db.AttendanceCheckInAttempts.SingleAsync(
|
||||
x => x.AttendanceSheetId == qrSheet.Id);
|
||||
Assert.True(qrAttempt.IsSuccessful);
|
||||
Assert.Equal("android", qrAttempt.DevicePlatform);
|
||||
Assert.NotNull(qrAttempt.DeviceIdentifierHash);
|
||||
|
||||
var secondStudentController = new AttendanceController(
|
||||
db,
|
||||
new StudentDataScope(secondStudentUserId));
|
||||
Assert.IsType<OkObjectResult>(
|
||||
await secondStudentController.CheckIn(
|
||||
new AttendanceCheckInRequest(
|
||||
null,
|
||||
qrToken,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"test-device-1",
|
||||
"android"),
|
||||
CancellationToken.None));
|
||||
var sharedDeviceAttempt = await db.AttendanceCheckInAttempts.SingleAsync(
|
||||
x => x.AttendanceSheetId == qrSheet.Id &&
|
||||
x.StudentId == secondStudent.Id);
|
||||
Assert.Contains(
|
||||
"SharedDevice",
|
||||
sharedDeviceAttempt.RiskFlags ?? string.Empty);
|
||||
|
||||
var locationRecord = new AttendanceRecord
|
||||
{
|
||||
@@ -282,6 +336,40 @@ public sealed class AttendanceControllerTests
|
||||
Assert.IsType<ConflictObjectResult>(outsideResult);
|
||||
Assert.Null(locationRecord.CheckInAt);
|
||||
|
||||
var inaccurateResult = await studentController.CheckIn(
|
||||
new AttendanceCheckInRequest(
|
||||
locationSheet.Id,
|
||||
null,
|
||||
39.9001m,
|
||||
116.4m,
|
||||
150),
|
||||
CancellationToken.None);
|
||||
Assert.IsType<ConflictObjectResult>(inaccurateResult);
|
||||
Assert.Null(locationRecord.CheckInAt);
|
||||
|
||||
var missingAccuracyResult = await studentController.CheckIn(
|
||||
new AttendanceCheckInRequest(
|
||||
locationSheet.Id,
|
||||
null,
|
||||
39.9001m,
|
||||
116.4m,
|
||||
null),
|
||||
CancellationToken.None);
|
||||
Assert.IsType<ConflictObjectResult>(missingAccuracyResult);
|
||||
Assert.Null(locationRecord.CheckInAt);
|
||||
var locationFailures = await db.AttendanceCheckInAttempts
|
||||
.Where(x =>
|
||||
x.AttendanceSheetId == locationSheet.Id &&
|
||||
!x.IsSuccessful)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.ToListAsync();
|
||||
Assert.Equal(3, locationFailures.Count);
|
||||
Assert.Contains(
|
||||
locationFailures,
|
||||
x => (x.RiskFlags ?? string.Empty).Contains(
|
||||
"RepeatedFailures",
|
||||
StringComparison.Ordinal));
|
||||
|
||||
var nearbyResult = await studentController.CheckIn(
|
||||
new AttendanceCheckInRequest(
|
||||
locationSheet.Id,
|
||||
|
||||
Reference in New Issue
Block a user