PAM lines written into login and passwd as blocks, so login unlocks the keyring on both workstations; no daemon of its own; gcr's ssh agent named for the session until the environment can say a runtime-directory path. Go tools unlocked, lock, collections and ssh-keys, never reading a secret.
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The module's manifest, read the way the catalogue reads it, for the manifest tests. The same in
|
|
// every desktop module that carries it.
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Capabilities []string `json:"capabilities"`
|
|
Requires []string `json:"requires"`
|
|
Claims []claim `json:"claims"`
|
|
Seats []any `json:"seats"`
|
|
Tools []string `json:"tools"`
|
|
Environment *environment `json:"environment"`
|
|
Shell []shellCode `json:"shell"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
type claim struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
}
|
|
|
|
type environment struct {
|
|
Variables map[string]string `json:"variables"`
|
|
Path []map[string]any `json:"path"`
|
|
}
|
|
|
|
type shellCode struct {
|
|
For string `json:"for"`
|
|
Slot string `json:"slot"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) manifest {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.DisallowUnknownFields()
|
|
var m manifest
|
|
if err := dec.Decode(&m); err != nil {
|
|
t.Fatalf("module.json: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m manifest) resource(t *testing.T, id string) map[string]any {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if r["id"] == id {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("no resource %q", id)
|
|
return nil
|
|
}
|
|
|
|
func (m manifest) packages() (present, absent []string) {
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "package" {
|
|
if r["absent"] == true {
|
|
absent = append(absent, r["package"].(string))
|
|
} else {
|
|
present = append(present, r["package"].(string))
|
|
}
|
|
}
|
|
}
|
|
return present, absent
|
|
}
|
|
|
|
// sameAsSource checks that a file resource's content is byte for byte the module's source file, so
|
|
// the readable file in the repository is what the machine gets.
|
|
func (m manifest) sameAsSource(t *testing.T, id, source string) {
|
|
t.Helper()
|
|
want, err := os.ReadFile(filepath.Join("..", "..", source))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := m.resource(t, id)
|
|
if r["type"] != "file" {
|
|
t.Fatalf("%s is a %v, not a file", id, r["type"])
|
|
}
|
|
if got, _ := r["content"].(string); got != string(want) {
|
|
t.Fatalf("resource %s's content is not %s: edit the source and copy it into module.json", id, source)
|
|
}
|
|
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
|
|
t.Fatalf("%s under the home is the account's", id)
|
|
}
|
|
}
|
|
|
|
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
|
|
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
|
|
func checkTheToolsAgree(t *testing.T, m manifest) {
|
|
t.Helper()
|
|
own, seat := map[string]bool{}, map[string]bool{}
|
|
for _, tool := range tools() {
|
|
if strings.Contains(tool.Name, ".") {
|
|
seat[tool.Name] = true
|
|
} else {
|
|
own[tool.Name] = true
|
|
}
|
|
if strings.TrimSpace(tool.Description) == "" {
|
|
t.Errorf("%s has no description", tool.Name)
|
|
}
|
|
}
|
|
listed := map[string]bool{}
|
|
for _, name := range m.Tools {
|
|
listed[name] = true
|
|
if !own[name] {
|
|
t.Errorf("module.json lists %s, which the bundle does not serve", name)
|
|
}
|
|
}
|
|
for name := range own {
|
|
if !listed[name] {
|
|
t.Errorf("the bundle serves %s, which module.json does not list", name)
|
|
}
|
|
if !strings.HasPrefix(name, strings.ReplaceAll(m.Module, "-", "_")+"_") {
|
|
t.Errorf("%s is not prefixed with the module's name", name)
|
|
}
|
|
}
|
|
promised := map[string]bool{}
|
|
for _, c := range m.Claims {
|
|
for _, verb := range c.Serves {
|
|
promised[c.Name+"."+verb] = true
|
|
if !seat[c.Name+"."+verb] {
|
|
t.Errorf("the claim on %s promises %s, which the bundle does not serve", c.Name, verb)
|
|
}
|
|
}
|
|
}
|
|
for name := range seat {
|
|
if !promised[name] {
|
|
t.Errorf("the bundle serves %s, which no claim promises", name)
|
|
}
|
|
}
|
|
var bundle map[string]any
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "bundle" {
|
|
bundle = a
|
|
}
|
|
}
|
|
if bundle == nil || bundle["language"] != "go" || bundle["system"] != "arch" ||
|
|
bundle["from"] != "cmd/"+m.Module+"-tools" || bundle["binary"] != m.Module+"-tools" {
|
|
t.Errorf("the Go tools bundle: %v", bundle)
|
|
}
|
|
}
|
|
|
|
// checkNoSecretsOrInstallationNames refuses what a catalogue manifest must never carry.
|
|
func checkNoSecretsOrInstallationNames(t *testing.T) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := strings.ToLower(string(raw))
|
|
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox.be", "api_key", ".hal/", "greenclip daemon"} {
|
|
if strings.Contains(s, never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|