1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
package main
import (
"strings"
"testing"
)
// An app name is simultaneously a state directory name, a systemd unit name,
// the PORTABLE_PREFIXES value in the image, and a DNS label. validateAppName is
// what keeps those from disagreeing, so the cases below are the ones where they
// would otherwise drift apart.
func TestValidateAppName(t *testing.T) {
for _, tc := range []struct {
name string
in string
ok bool
// want is a substring the error must mention, so that a rejection is
// checked for the *right* reason rather than merely being a rejection.
want string
}{
// Every app packed in default.nix, plus both apps deployed on legosi.
// If any of these ever fails, the check has become stricter than the
// apps that already exist.
{name: "packed hosty-hello", in: "hosty-hello", ok: true},
{name: "packed rss-parrot", in: "rss-parrot", ok: true},
{name: "packed pocket-id", in: "pocket-id", ok: true},
{name: "packed asciinema-server", in: "asciinema-server", ok: true},
{name: "packed vaultwarden", in: "vaultwarden", ok: true},
{name: "single letter", in: "a", ok: true},
{name: "single digit", in: "7", ok: true},
{name: "digits and letters", in: "app2", ok: true},
{name: "leading digit", in: "2fa", ok: true},
{name: "several inner dashes", in: "a-b-c-d", ok: true},
{name: "max length", in: strings.Repeat("a", 63), ok: true},
// Length: the DNS label limit, not an arbitrary one.
{name: "one over max length", in: strings.Repeat("a", 64), ok: false, want: "at most 63"},
{name: "empty", in: "", ok: false, want: "must not be empty"},
// Uppercase is rejected, not normalised: DNS is case-insensitive but
// paths are not, so it would be one subdomain and two images.
{name: "uppercase initial", in: "MyApp", ok: false, want: "lowercase"},
{name: "uppercase single letter", in: "A", ok: false, want: "lowercase"},
{name: "all caps", in: "APP", ok: false, want: "lowercase"},
// Dash placement: an RFC 1123 label may not start or end with one.
{name: "leading dash", in: "-lead", ok: false, want: "start or end with a dash"},
{name: "trailing dash", in: "trail-", ok: false, want: "start or end with a dash"},
{name: "only a dash", in: "-", ok: false, want: "start or end with a dash"},
// Characters that are legal in a path or unit name but not in a label.
{name: "underscore", in: "has_underscore", ok: false, want: "only lowercase"},
{name: "dot", in: "foo.bar", ok: false, want: "only lowercase"},
{name: "slash would escape the state dir", in: "foo/bar", ok: false, want: "only lowercase"},
{name: "space", in: "foo bar", ok: false, want: "only lowercase"},
{name: "at sign is a systemd template marker", in: "foo@bar", ok: false, want: "only lowercase"},
{name: "parent directory", in: "..", ok: false, want: "only lowercase"},
{name: "non-ascii", in: "café", ok: false, want: "only lowercase"},
// -fs would collide with the companion FUSE unit hosty synthesizes for
// every app: app "foo-fs" and app "foo"'s foo-fs.service are one name.
{name: "fs suffix collides with companion unit", in: "foo-fs", ok: false, want: "-fs"},
{name: "fs suffix on a real app name", in: "rss-parrot-fs", ok: false, want: "-fs"},
// ...but "fs" alone, and names merely containing fs, are fine.
{name: "bare fs is not a suffix collision", in: "fs", ok: true},
{name: "fs inside the name", in: "fsck", ok: true},
{name: "fs without the dash", in: "myfs", ok: true},
// Reserved infrastructure subdomains.
{name: "reserved www", in: "www", ok: false, want: "reserved"},
{name: "reserved admin", in: "admin", ok: false, want: "reserved"},
{name: "reserved api", in: "api", ok: false, want: "reserved"},
{name: "reserved hosty", in: "hosty", ok: false, want: "reserved"},
// Only the exact names are reserved; hosty-hello must keep working.
{name: "reserved name as a prefix is fine", in: "hosty-hello", ok: true},
{name: "reserved name as a suffix is fine", in: "my-api", ok: true},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateAppName(tc.in)
if tc.ok {
if err != nil {
t.Fatalf("validateAppName(%q) = %v, want nil", tc.in, err)
}
return
}
if err == nil {
t.Fatalf("validateAppName(%q) = nil, want an error", tc.in)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("validateAppName(%q) = %q, want it to mention %q",
tc.in, err, tc.want)
}
})
}
}
|