halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
test('reads an address as people type it, HTTPS unless they say not', () {
|
|
|
|
|
for (final address in [
|
|
|
|
|
'learn.example.org',
|
|
|
|
|
'https://learn.example.org',
|
|
|
|
|
' HTTPS://Learn.Example.org/ ',
|
|
|
|
|
]) {
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse(address)?.url,
|
|
|
|
|
Uri.parse('https://learn.example.org/'),
|
|
|
|
|
reason: address,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse('http://localhost:8080')?.url,
|
|
|
|
|
Uri.parse('http://localhost:8080/'),
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse('example.org/learn?x=1#top')?.url,
|
|
|
|
|
Uri.parse('https://example.org/learn/'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-11-09 23:15:27 +01:00
|
|
|
test('reaches this machine over HTTP where no scheme is typed, as a '
|
|
|
|
|
"server running on it has no certificate (the developer's review, "
|
|
|
|
|
'9 Nov 2026)', () {
|
|
|
|
|
for (final (address, url) in [
|
|
|
|
|
('localhost:8080', 'http://localhost:8080/'),
|
|
|
|
|
('LOCALHOST', 'http://localhost/'),
|
|
|
|
|
('127.0.0.1:8080', 'http://127.0.0.1:8080/'),
|
|
|
|
|
('[::1]:8080', 'http://[::1]:8080/'),
|
|
|
|
|
('https://localhost:8443', 'https://localhost:8443/'),
|
|
|
|
|
]) {
|
|
|
|
|
expect(Instance.parse(address)?.url, Uri.parse(url), reason: address);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('takes an IP address as a host', () {
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse('192.168.1.10:8080')?.url,
|
|
|
|
|
Uri.parse('https://192.168.1.10:8080/'),
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse('http://[2001:db8::1]')?.url,
|
|
|
|
|
Uri.parse('http://[2001:db8::1]/'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
test('refuses what is no address', () {
|
|
|
|
|
for (final address in [
|
|
|
|
|
'',
|
|
|
|
|
' ',
|
|
|
|
|
'learn example org',
|
|
|
|
|
'ftp://example.org',
|
|
|
|
|
'https://',
|
|
|
|
|
'https://someone@example.org',
|
2026-11-09 23:15:27 +01:00
|
|
|
// A host of one word that isn't this machine, which no instance on
|
|
|
|
|
// the internet has: refused as typed rather than looked for
|
|
|
|
|
// (the developer's review, 9 Nov 2026).
|
|
|
|
|
'todo',
|
|
|
|
|
'https://learn',
|
|
|
|
|
'learn.',
|
|
|
|
|
'.org',
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
]) {
|
|
|
|
|
expect(Instance.parse(address), isNull, reason: address);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is named by its host, and its port if it has one', () {
|
|
|
|
|
expect(Instance.parse('learn.halkyon.eu')!.name, 'learn.halkyon.eu');
|
|
|
|
|
expect(Instance.parse('localhost:8080')!.name, 'localhost:8080');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is the same instance however its address was written', () {
|
|
|
|
|
expect(
|
|
|
|
|
Instance.parse('learn.example.org'),
|
|
|
|
|
Instance.parse('https://LEARN.example.org/'),
|
|
|
|
|
);
|
|
|
|
|
});
|
fix(app)!: instances over plain HTTP are refused, but on this machine
What the app sends an instance (the proofs that sign in, the keys'
wrappings, every request a session carries) must not be read or changed on
the way, so an instance is reached over HTTPS, or over plain HTTP only on
this machine (localhost, 127.0.0.0/8, ::1), as Jellyfin servers are. An
address over plain HTTP elsewhere is refused before anything is sent there.
One kept from before is never opened: the app says why, and offers another
instance or removing it from the device, never dropping it silently.
Development builds still use their own instance over HTTP (an emulator
reaches the developer's machine as 10.0.2.2); releases never.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 14:35:21 +02:00
|
|
|
|
|
|
|
|
test('is reached securely over HTTPS, or over HTTP on this machine only', () {
|
|
|
|
|
for (final address in [
|
|
|
|
|
'learn.example.org',
|
|
|
|
|
'https://learn.example.org:8443/learn',
|
|
|
|
|
'http://localhost:8080',
|
|
|
|
|
'http://127.0.0.1:8080',
|
|
|
|
|
'http://127.1.2.3',
|
|
|
|
|
'http://[::1]:8080',
|
|
|
|
|
]) {
|
|
|
|
|
expect(Instance.parse(address)!.isSecure, isTrue, reason: address);
|
|
|
|
|
}
|
|
|
|
|
for (final address in [
|
|
|
|
|
'http://learn.example.org',
|
|
|
|
|
'http://10.0.2.2:8080',
|
|
|
|
|
'http://192.168.1.10:8080',
|
|
|
|
|
'http://localhost.example.org',
|
|
|
|
|
'http://[::2]',
|
|
|
|
|
]) {
|
|
|
|
|
expect(Instance.parse(address)!.isSecure, isFalse, reason: address);
|
|
|
|
|
}
|
|
|
|
|
});
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
}
|