feat(todo): the server on halkyon_base
Todo's server includes halkyon_base (ADR 0014), configured for Todo: its basics say it serves `todo`, sign-in mails name Halkyon Todo, the source is this repository, minimumAppBuild 1. Authentication through the module (anonymous, GitHub, email codes, server-side sessions) in place of the scaffold's email-and-password provider; the module's pruning scheduled once however often the server starts. The scaffold's greeting, its Flutter web hosting, cloud storage and Docker files go; session logs are never persisted. Todo was never deployed, so its migrations start again with one initial migration. The root's lints apply, with the four constructor rules off as on Learn's server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
38770569ea
commit
28dd3ea65a
4 changed files with 1047 additions and 1741 deletions
|
|
@ -1,24 +0,0 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
// Import the generated test helper file, it contains everything you need.
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
// This is an example test that uses the `withServerpod` test helper.
|
||||
// `withServerpod` enables you to call your endpoints directly from the test like regular functions.
|
||||
// Note that after adding or modifying an endpoint, you will need to run
|
||||
// `serverpod generate` to update the test tools code.
|
||||
// Refer to the docs for more information on how to use the test helper.
|
||||
withServerpod('Given Greeting endpoint', (sessionBuilder, endpoints) {
|
||||
test(
|
||||
'when calling `hello` with name then returned greeting includes name',
|
||||
() async {
|
||||
// Call the endpoint method by using the `endpoints` parameter and
|
||||
// pass `sessionBuilder` as a first argument. Refer to the docs on
|
||||
// how to use the `sessionBuilder` to set up different test scenarios.
|
||||
final greeting = await endpoints.greeting.hello(sessionBuilder, 'Bob');
|
||||
expect(greeting.message, 'Hello Bob');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_todo_server/src/todo_base.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
/// Sets the server up as it runs (lib/server.dart): halkyon_base as Todo
|
||||
/// configures it (ADR 0014), and authentication. On the pod [sessionBuilder]'s
|
||||
/// sessions run on, not on `Serverpod.instance`: that is the pod made last.
|
||||
void useTodoBase(TestSessionBuilder sessionBuilder) {
|
||||
final pod = sessionBuilder.build().serverpod;
|
||||
HalkyonBase.configure(pod, todoBase());
|
||||
AuthServices.set(
|
||||
tokenManagerBuilders: tokenManagerBuilders,
|
||||
identityProviderBuilders: identityProviderBuilders(pod),
|
||||
);
|
||||
}
|
||||
|
|
@ -11,13 +11,10 @@
|
|||
// ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _ida;
|
||||
import 'dart:io' as _idi;
|
||||
import 'package:halkyon_todo_server/src/generated/greetings/greeting.dart'
|
||||
as _igy4f3bz;
|
||||
import 'package:halkyon_todo_server/src/generated/future_calls.dart'
|
||||
as _i772x17y;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
|
||||
as _iacs;
|
||||
import 'package:serverpod_test/serverpod_test.dart' as _ist;
|
||||
import 'package:halkyon_todo_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_todo_server/src/generated/endpoints.dart';
|
||||
|
|
@ -152,9 +149,7 @@ void withServerpod(
|
|||
}
|
||||
|
||||
class TestEndpoints {
|
||||
late final _EmailIdpEndpoint emailIdp;
|
||||
|
||||
late final _GreetingEndpoint greeting;
|
||||
late final futureCalls = _FutureCalls();
|
||||
}
|
||||
|
||||
class _InternalTestEndpoints extends TestEndpoints
|
||||
|
|
@ -163,332 +158,41 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
void initialize(
|
||||
_is.SerializationManager serializationManager,
|
||||
_is.EndpointDispatch endpoints,
|
||||
) {
|
||||
emailIdp = _EmailIdpEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
greeting = _GreetingEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
}
|
||||
) {}
|
||||
}
|
||||
|
||||
class _EmailIdpEndpoint {
|
||||
_EmailIdpEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> login(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'login',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'login',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'email': email,
|
||||
'password': password,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_is.UuidValue> startRegistration(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String email,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'startRegistration',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'startRegistration',
|
||||
parameters: _ist.testObjectToJson({'email': email}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<String> verifyRegistrationCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue accountRequestId,
|
||||
required String verificationCode,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'verifyRegistrationCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'verifyRegistrationCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'accountRequestId': accountRequestId,
|
||||
'verificationCode': verificationCode,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<String>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_iacs.AuthSuccess> finishRegistration(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String registrationToken,
|
||||
required String password,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'finishRegistration',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'finishRegistration',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'registrationToken': registrationToken,
|
||||
'password': password,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_iacs.AuthSuccess>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<_is.UuidValue> startPasswordReset(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String email,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'startPasswordReset',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'startPasswordReset',
|
||||
parameters: _ist.testObjectToJson({'email': email}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_is.UuidValue>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<String> verifyPasswordResetCode(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _is.UuidValue passwordResetRequestId,
|
||||
required String verificationCode,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'verifyPasswordResetCode',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'verifyPasswordResetCode',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'passwordResetRequestId': passwordResetRequestId,
|
||||
'verificationCode': verificationCode,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<String>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<void> finishPasswordReset(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required String finishPasswordResetToken,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'finishPasswordReset',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'finishPasswordReset',
|
||||
parameters: _ist.testObjectToJson({
|
||||
'finishPasswordResetToken': finishPasswordResetToken,
|
||||
'newPassword': newPassword,
|
||||
}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<void>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_ida.Future<bool> hasAccount(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'emailIdp',
|
||||
method: 'hasAccount',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'emailIdp',
|
||||
methodName: 'hasAccount',
|
||||
parameters: _ist.testObjectToJson({}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<bool>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
class _FutureCalls {
|
||||
late final pruning = _PruningFutureCall();
|
||||
}
|
||||
|
||||
class _GreetingEndpoint {
|
||||
_GreetingEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
||||
final _is.EndpointDispatch _endpointDispatch;
|
||||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Future<_igy4f3bz.Greeting> hello(
|
||||
class _PruningFutureCall {
|
||||
Future<void> pruneAnonymousPeople(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
String name,
|
||||
) async {
|
||||
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
|
||||
endpoint: 'greeting',
|
||||
method: 'hello',
|
||||
);
|
||||
try {
|
||||
var _localCallContext = await _endpointDispatch.getMethodCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'greeting',
|
||||
methodName: 'hello',
|
||||
parameters: _ist.testObjectToJson({'name': name}),
|
||||
serializationManager: _serializationManager,
|
||||
);
|
||||
var _localReturnValue =
|
||||
await (_localCallContext.method.call(
|
||||
_localUniqueSession,
|
||||
_localCallContext.arguments,
|
||||
)
|
||||
as _ida.Future<_igy4f3bz.Greeting>);
|
||||
return _localReturnValue;
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
});
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _i772x17y.PruningPruneAnonymousPeopleFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> forgetSignUpAttempts(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _i772x17y.PruningForgetSignUpAttemptsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_todo_server/src/pruning.dart';
|
||||
import 'package:serverpod/protocol.dart' show FutureCallEntry;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'support/todo_fixtures.dart';
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
"Todo's server",
|
||||
// Signing in runs concurrent transactions (the rate limit and the new
|
||||
// account), which the test's database rollback can't wrap. So these
|
||||
// tests don't roll back; they delete what they create instead.
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
(sessionBuilder, endpoints) {
|
||||
late Serverpod pod;
|
||||
|
||||
setUp(() async {
|
||||
useTodoBase(sessionBuilder);
|
||||
pod = sessionBuilder.build().serverpod;
|
||||
await _deleteAll(sessionBuilder.build());
|
||||
});
|
||||
tearDown(() => _deleteAll(sessionBuilder.build()));
|
||||
|
||||
/// Calls [method] of the module's endpoint [endpoint] as a request
|
||||
/// reaches it: through the server's own dispatch, under the name the
|
||||
/// app's client calls (`halkyon_base.<endpoint>`).
|
||||
Future<Object?> call(
|
||||
String endpoint,
|
||||
String method, [
|
||||
Map<String, Object?> parameters = const {},
|
||||
]) {
|
||||
final connector = pod.endpoints.getConnectorByName(
|
||||
'halkyon_base.$endpoint',
|
||||
);
|
||||
final methodConnector =
|
||||
connector!.methodConnectors[method]! as MethodConnector;
|
||||
return methodConnector.call(sessionBuilder.build(), parameters);
|
||||
}
|
||||
|
||||
test('its basics name the app it serves, `todo`, so only Todo takes it '
|
||||
'for an instance of its own', () async {
|
||||
final basics = (await call('instance', 'basics'))! as InstanceBasics;
|
||||
|
||||
expect(basics.app, 'todo');
|
||||
expect(basics.minimumAppBuild, 1);
|
||||
expect(
|
||||
basics.sourceCodeUrl,
|
||||
'https://code.halkyon.app/project-halkyon/halkyon',
|
||||
);
|
||||
});
|
||||
|
||||
test('a device starts without an account through the module, as a '
|
||||
'person of their own', () async {
|
||||
final success = (await call('anonymousIdp', 'login'))! as AuthSuccess;
|
||||
|
||||
final person = await AuthServices.instance.authUsers.get(
|
||||
sessionBuilder.build(),
|
||||
authUserId: success.authUserId,
|
||||
);
|
||||
expect(person.id, success.authUserId);
|
||||
});
|
||||
|
||||
test(
|
||||
'the pruning is scheduled once, however often the server starts',
|
||||
() async {
|
||||
await schedulePruning(pod);
|
||||
await schedulePruning(pod);
|
||||
|
||||
final scheduled = await FutureCallEntry.db.find(
|
||||
sessionBuilder.build(),
|
||||
where: (t) => t.identifier.inSet(pruningIdentifiers.toSet()),
|
||||
);
|
||||
expect(
|
||||
scheduled.map((entry) => entry.identifier),
|
||||
unorderedEquals(pruningIdentifiers),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes every rate-limit record, every person (their anonymous accounts
|
||||
/// and sessions go with them, `ON DELETE CASCADE`) and every future call.
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AuthUser.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
await FutureCallEntry.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue