mesh/merge-gate head df821c8a4ab6 merged into trunk 49caf17ff56e — pass: builds only-office → novox; no bus step; every machine composes with the change as…
mesh/repo-check head df821c8a4ab6 merged into trunk 49caf17ff56e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
Issue 1843 kept the old /var/lib/only-office/lib declared only to close it to 0700 while it held data. Its contents are deleted now, so the declaration has no job left; mesh-host removes the empty directory once it is no longer declared.
310 lines
10 KiB
Go
310 lines
10 KiB
Go
package office
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// 1830/AC1 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest is parsed, then it offers document-server with
|
|
// credential own jwt, jwt says taken at-start, and serves document-server on port 9070 with a
|
|
// public-root.
|
|
func TestManifestOffersDocumentServer(t *testing.T) {
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatalf("reading module.json: %v", err)
|
|
}
|
|
|
|
m, err := catalogue.ParseManifest(raw)
|
|
if err != nil {
|
|
t.Fatalf("parsing module.json: %v", err)
|
|
}
|
|
|
|
var offer *catalogue.Offer
|
|
for i := range m.Provides {
|
|
if m.Provides[i].Name == "document-server" {
|
|
offer = &m.Provides[i]
|
|
break
|
|
}
|
|
}
|
|
if offer == nil {
|
|
t.Fatal("manifest does not provide document-server")
|
|
}
|
|
if offer.Scope != catalogue.ScopeMesh {
|
|
t.Errorf("document-server scope = %q, want %q", offer.Scope, catalogue.ScopeMesh)
|
|
}
|
|
if offer.Credential == nil || offer.Credential.Own != "jwt" {
|
|
t.Errorf("document-server credential = %+v, want own jwt", offer.Credential)
|
|
}
|
|
|
|
own, ok := m.OwnSecrets["jwt"]
|
|
if !ok {
|
|
t.Fatal("manifest declares no own-secret jwt")
|
|
}
|
|
if own.Taken != catalogue.TakenAtStart {
|
|
t.Errorf("own-secrets.jwt.taken = %q, want %q", own.Taken, catalogue.TakenAtStart)
|
|
}
|
|
|
|
serve, ok := m.Serves["document-server"]
|
|
if !ok {
|
|
t.Fatal("manifest serves no document-server")
|
|
}
|
|
if scheme, _ := serve["scheme"].(string); scheme != "http" {
|
|
t.Errorf("serves.document-server.scheme = %q, want http", scheme)
|
|
}
|
|
if port, _ := serve["port"].(float64); port != 9070 {
|
|
t.Errorf("serves.document-server.port = %v, want 9070", serve["port"])
|
|
}
|
|
if root, _ := serve["public-root"].(string); root != "https://${bound:route:name}/" {
|
|
t.Errorf("serves.document-server.public-root = %q, want https://${bound:route:name}/", root)
|
|
}
|
|
}
|
|
|
|
// 1837/AC1 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest and its server.env template are read, then the
|
|
// document server's secure-link secret is an own secret taken at start, and server.env hands it to
|
|
// the container as SECURE_LINK_SECRET, so its document service and its nginx sign and check
|
|
// Editor.bin links with one value across every restart.
|
|
func TestSecureLinkSecretIsPinned(t *testing.T) {
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatalf("reading module.json: %v", err)
|
|
}
|
|
m, err := catalogue.ParseManifest(raw)
|
|
if err != nil {
|
|
t.Fatalf("parsing module.json: %v", err)
|
|
}
|
|
own, ok := m.OwnSecrets["secure-link"]
|
|
if !ok {
|
|
t.Fatal("manifest declares no own-secret secure-link")
|
|
}
|
|
if own.Taken != catalogue.TakenAtStart {
|
|
t.Errorf("own-secrets.secure-link.taken = %q, want %q", own.Taken, catalogue.TakenAtStart)
|
|
}
|
|
|
|
env, err := os.ReadFile("files/server.env")
|
|
if err != nil {
|
|
t.Fatalf("reading files/server.env: %v", err)
|
|
}
|
|
if !strings.Contains(string(env), "\nSECURE_LINK_SECRET=${secret:secure-link}\n") {
|
|
t.Errorf("files/server.env does not set SECURE_LINK_SECRET=${secret:secure-link}:\n%s", env)
|
|
}
|
|
}
|
|
|
|
// 1837/AC3 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest is read, then the directory mounted at
|
|
// /var/lib/onlyoffice (server-lib, where the document service writes the converted files nginx
|
|
// serves) lets other users pass through it (mode 0711): the image makes it the document service's
|
|
// own, and nginx runs as another user, so a mode of 0700 makes nginx answer 403 "Permission denied"
|
|
// on Editor.bin. Host accounts are kept out by the box above it (1843/AC1).
|
|
func TestLibDirectoryLetsNginxThrough(t *testing.T) {
|
|
m := readLayout(t)
|
|
src := m.volumeSource(t, "/var/lib/onlyoffice")
|
|
if src != "${dir:server-lib}" {
|
|
t.Fatalf("/var/lib/onlyoffice is mounted from %q, want ${dir:server-lib}", src)
|
|
}
|
|
d := m.dir(t, "server-lib")
|
|
if d.Mode != "0711" {
|
|
t.Errorf("server-lib directory mode = %q, want 0711 so nginx can reach the document service's cache", d.Mode)
|
|
}
|
|
}
|
|
|
|
// 1843/AC1 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest is read, then it declares a directory box with no
|
|
// place, no path and no owner (root, /var/lib/only-office/box) and mode 0700, and every host source of
|
|
// the server container's volumes is a directory whose path is ${dir:box}/<name>, so no account but
|
|
// root can pass to anything the container writes: the image opens what is below each mount to every
|
|
// account on every start.
|
|
func TestContainerDirectoriesSitInARootOnlyBox(t *testing.T) {
|
|
m := readLayout(t)
|
|
box := m.dir(t, "box")
|
|
if box.Place != "" || box.Path != "" || box.Owner != "" {
|
|
t.Errorf("box = place %q, path %q, owner %q, want none of them (root, at <root>/only-office/box)", box.Place, box.Path, box.Owner)
|
|
}
|
|
if box.Mode != "0700" {
|
|
t.Errorf("box mode = %q, want 0700", box.Mode)
|
|
}
|
|
c := m.container(t, "server")
|
|
if len(c.Volumes) == 0 {
|
|
t.Fatal("the server container mounts nothing")
|
|
}
|
|
for _, v := range c.Volumes {
|
|
src, target := splitVolume(v)
|
|
id, ok := strings.CutPrefix(src, "${dir:")
|
|
if !ok || !strings.HasSuffix(id, "}") {
|
|
t.Errorf("volume %q has a host source that is not a directory of the module", v)
|
|
continue
|
|
}
|
|
d := m.dir(t, strings.TrimSuffix(id, "}"))
|
|
if !strings.HasPrefix(d.Path, "${dir:box}/") || strings.Count(d.Path, "/") != 1 {
|
|
t.Errorf("%s (mounted at %s) has path %q, want ${dir:box}/<name>", d.ID, target, d.Path)
|
|
}
|
|
if d.Owner != "" {
|
|
t.Errorf("%s is owned by %q, want root", d.ID, d.Owner)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1843/AC2 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest is read, then the module root (state) is still
|
|
// root-owned, mode 0711, place ".", and the converter's secrets and environment still name
|
|
// ${dir:state}/jwt.secret and ${dir:state}/broker-converter.secret: the converter passes the root and
|
|
// nothing below it, and no account but root owns a directory on any path.
|
|
func TestConverterPassesOnlyTheRoot(t *testing.T) {
|
|
m := readLayout(t)
|
|
state := m.dir(t, "state")
|
|
if state.Place != "." || state.Owner != "" || state.Mode != "0711" {
|
|
t.Errorf("state = place %q, owner %q, mode %q, want place ., root, 0711", state.Place, state.Owner, state.Mode)
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r.Type == "directory" && r.Owner != "" {
|
|
t.Errorf("directory %s is owned by %q, want root", r.ID, r.Owner)
|
|
}
|
|
}
|
|
for name, want := range map[string]string{"jwt": "${dir:state}/jwt.secret", "broker-converter": "${dir:state}/broker-converter.secret"} {
|
|
if got := m.OwnSecrets[name].Path; got != want {
|
|
t.Errorf("own-secrets.%s.path = %q, want %q", name, got, want)
|
|
}
|
|
}
|
|
var conv *layoutResource
|
|
for i := range m.Resources {
|
|
if m.Resources[i].ID == "converter" {
|
|
conv = &m.Resources[i]
|
|
}
|
|
}
|
|
if conv == nil {
|
|
t.Fatal("manifest declares no converter")
|
|
}
|
|
if conv.Env["ONLY_OFFICE_JWT"] != "${dir:state}/jwt.secret" || conv.Env["ONLY_OFFICE_BROKER"] != "${dir:state}/broker-converter.secret" {
|
|
t.Errorf("converter env = %v, want its secrets in ${dir:state}", conv.Env)
|
|
}
|
|
}
|
|
|
|
// 1854/AC1 (unit: manifest check)
|
|
// Given the catalogue, when only-office's manifest is read, then it declares no directory lib (the old
|
|
// copy the container used before issue 1843, emptied on the operator's word), no container volume names
|
|
// ${dir:lib}, and every data entry still points at a directory the server container mounts.
|
|
func TestOldLibIsGone(t *testing.T) {
|
|
m := readLayout(t)
|
|
for _, r := range m.Resources {
|
|
if r.ID == "lib" && r.Type == "directory" {
|
|
t.Errorf("the manifest still declares the directory lib (path %q, mode %q)", r.Path, r.Mode)
|
|
}
|
|
}
|
|
mounted := map[string]bool{}
|
|
for _, v := range m.container(t, "server").Volumes {
|
|
src, _ := splitVolume(v)
|
|
mounted[src] = true
|
|
}
|
|
if mounted["${dir:lib}"] {
|
|
t.Error("the server container still mounts ${dir:lib}")
|
|
}
|
|
for _, d := range m.Data.Own {
|
|
if !mounted[d.Path] {
|
|
t.Errorf("data entry %s points at %q, which the server container does not mount", d.ID, d.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1854/AC2 (unit: manifest check)
|
|
// Given the catalogue, when only-office's README is read, then it names no ${dir:lib} and no longer
|
|
// describes the old copy as kept.
|
|
func TestReadmeNamesNoOldLib(t *testing.T) {
|
|
raw, err := os.ReadFile("README.md")
|
|
if err != nil {
|
|
t.Fatalf("reading README.md: %v", err)
|
|
}
|
|
if strings.Contains(string(raw), "${dir:lib}") {
|
|
t.Error("README.md still names ${dir:lib}")
|
|
}
|
|
if strings.Contains(string(raw), "/var/lib/only-office/lib") {
|
|
t.Error("README.md still describes the old copy at /var/lib/only-office/lib")
|
|
}
|
|
}
|
|
|
|
type layoutResource struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Mode string `json:"mode"`
|
|
Place string `json:"place"`
|
|
Path string `json:"path"`
|
|
Owner string `json:"owner"`
|
|
Volumes []string `json:"volumes"`
|
|
Env map[string]string `json:"env"`
|
|
}
|
|
|
|
type layout struct {
|
|
Resources []layoutResource `json:"resources"`
|
|
OwnSecrets map[string]struct {
|
|
Path string `json:"path"`
|
|
} `json:"own-secrets"`
|
|
Data struct {
|
|
Own []struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
} `json:"own"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func readLayout(t *testing.T) layout {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatalf("reading module.json: %v", err)
|
|
}
|
|
if _, err := catalogue.ParseManifest(raw); err != nil {
|
|
t.Fatalf("parsing module.json: %v", err)
|
|
}
|
|
var m layout
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatalf("parsing module.json: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (m layout) dir(t *testing.T, id string) layoutResource {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if r.ID == id && r.Type == "directory" {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("manifest declares no directory %s", id)
|
|
return layoutResource{}
|
|
}
|
|
|
|
func (m layout) container(t *testing.T, id string) layoutResource {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if r.ID == id && r.Type == "container" {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("manifest declares no container %s", id)
|
|
return layoutResource{}
|
|
}
|
|
|
|
func (m layout) volumeSource(t *testing.T, target string) string {
|
|
t.Helper()
|
|
for _, v := range m.container(t, "server").Volumes {
|
|
if src, to := splitVolume(v); to == target {
|
|
return src
|
|
}
|
|
}
|
|
t.Fatalf("the server container mounts nothing at %s", target)
|
|
return ""
|
|
}
|
|
|
|
// splitVolume is a volume's host source and container target; the source's own ${dir:…} holds a colon.
|
|
func splitVolume(v string) (src, target string) {
|
|
i := strings.LastIndex(v, ":")
|
|
if i < 0 {
|
|
return v, ""
|
|
}
|
|
return v[:i], v[i+1:]
|
|
}
|