Feature 16: Google Calendar Sync (#130)
CI / ci (push) Successful in 4m51s

Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #130.
This commit is contained in:
2026-10-08 03:13:21 -04:00
committed by approver_agent
parent 5ac5982c1f
commit 7003ff92ec
65 changed files with 8092 additions and 222 deletions
+12
View File
@@ -29,6 +29,9 @@ const (
EnvOIDCClientSecretFile = "MEALQUEST_OIDC_CLIENT_SECRET_FILE"
EnvOIDCAllowedGroups = "MEALQUEST_OIDC_ALLOWED_GROUPS"
EnvGoogleClientID = "MEALQUEST_GOOGLE_CLIENT_ID"
EnvGoogleClientSecretFile = "MEALQUEST_GOOGLE_CLIENT_SECRET_FILE"
EnvSessionIdle = "MEALQUEST_SESSION_IDLE"
EnvSessionMax = "MEALQUEST_SESSION_MAX"
@@ -61,6 +64,12 @@ type Config struct {
// OIDCAllowedGroups lists the groups whose members may sign in.
OIDCAllowedGroups []string
// GoogleClientID and GoogleClientSecret are the Google OAuth web client
// Push to Calendar connects with. Its redirect URI is PublicURL's origin
// followed by /api/v1/integrations/google/callback.
GoogleClientID string
GoogleClientSecret Secret
// SessionIdle ends a session left unused this long.
SessionIdle time.Duration
// SessionMax ends every session this long after sign-in, used or not.
@@ -110,6 +119,9 @@ func load(lookup func(string) (string, bool)) (Config, error) {
OIDCClientSecret: r.secretFile(EnvOIDCClientSecretFile),
OIDCAllowedGroups: r.list(EnvOIDCAllowedGroups),
GoogleClientID: r.str(EnvGoogleClientID),
GoogleClientSecret: r.secretFile(EnvGoogleClientSecretFile),
SessionIdle: r.sessionDuration(EnvSessionIdle, DefaultSessionIdle),
SessionMax: r.sessionDuration(EnvSessionMax, DefaultSessionMax),
+47 -1
View File
@@ -29,6 +29,10 @@ func TestLoad(t *testing.T) {
if err := os.WriteFile(clientSecretPath, []byte("client-s3cret\n"), 0o600); err != nil {
t.Fatal(err)
}
googleSecretPath := filepath.Join(dir, "google_client_secret")
if err := os.WriteFile(googleSecretPath, []byte("google-s3cret\n"), 0o600); err != nil {
t.Fatal(err)
}
emptyPath := filepath.Join(dir, "empty_secret")
if err := os.WriteFile(emptyPath, []byte("\n"), 0o600); err != nil {
t.Fatal(err)
@@ -46,6 +50,9 @@ func TestLoad(t *testing.T) {
EnvOIDCClientID: "mealquest",
EnvOIDCClientSecretFile: clientSecretPath,
EnvOIDCAllowedGroups: "mealquest-admin",
EnvGoogleClientID: "mealquest.apps.googleusercontent.com",
EnvGoogleClientSecretFile: googleSecretPath,
}
}
with := func(changes map[string]string, unset ...string) map[string]string {
@@ -94,6 +101,7 @@ func TestLoad(t *testing.T) {
badVars: []string{
EnvHTTPAddr, EnvPublicURL, EnvDataDir, EnvShutdownTimeout, EnvSecretKeyFile,
EnvOIDCIssuer, EnvOIDCClientID, EnvOIDCClientSecretFile, EnvOIDCAllowedGroups,
EnvGoogleClientID, EnvGoogleClientSecretFile,
},
},
{
@@ -211,6 +219,31 @@ func TestLoad(t *testing.T) {
env: with(map[string]string{"MEALQUEST_SECRET_KEY": "inline"}),
badVars: []string{"MEALQUEST_SECRET_KEY"},
},
{
name: "Google client ID missing",
env: with(nil, EnvGoogleClientID),
badVars: []string{EnvGoogleClientID},
},
{
name: "Google client secret file missing",
env: with(nil, EnvGoogleClientSecretFile),
badVars: []string{EnvGoogleClientSecretFile},
},
{
name: "Google client secret file unreadable",
env: with(map[string]string{EnvGoogleClientSecretFile: unreadablePath}),
badVars: []string{EnvGoogleClientSecretFile},
},
{
name: "Google client secret file empty",
env: with(map[string]string{EnvGoogleClientSecretFile: emptyPath}),
badVars: []string{EnvGoogleClientSecretFile},
},
{
name: "inline Google client secret rejected",
env: with(map[string]string{"MEALQUEST_GOOGLE_CLIENT_SECRET": "inline"}),
badVars: []string{"MEALQUEST_GOOGLE_CLIENT_SECRET"},
},
{
name: "inline OIDC client secret rejected",
env: with(map[string]string{"MEALQUEST_OIDC_CLIENT_SECRET": "inline"}),
@@ -409,6 +442,10 @@ func TestLoadValues(t *testing.T) {
if err := os.WriteFile(clientSecretPath, []byte("client-s3cret\n"), 0o600); err != nil {
t.Fatal(err)
}
googleSecretPath := filepath.Join(dir, "google_client_secret")
if err := os.WriteFile(googleSecretPath, []byte("google-s3cret\n"), 0o600); err != nil {
t.Fatal(err)
}
env := map[string]string{
EnvHTTPAddr: "127.0.0.1:9000",
EnvPublicURL: "http://localhost:9000",
@@ -421,6 +458,9 @@ func TestLoadValues(t *testing.T) {
EnvOIDCClientSecretFile: clientSecretPath,
EnvOIDCAllowedGroups: " mealquest-admin ,family",
EnvGoogleClientID: "mealquest.apps.googleusercontent.com",
EnvGoogleClientSecretFile: googleSecretPath,
EnvDefaultTZ: "Europe/London",
EnvSearXNGURL: "http://searxng:8080",
@@ -454,6 +494,12 @@ func TestLoadValues(t *testing.T) {
if cfg.OIDCClientSecret.Reveal() != "client-s3cret" {
t.Errorf("OIDCClientSecret.Reveal() = %q", cfg.OIDCClientSecret.Reveal())
}
if cfg.GoogleClientID != "mealquest.apps.googleusercontent.com" {
t.Errorf("GoogleClientID = %q", cfg.GoogleClientID)
}
if cfg.GoogleClientSecret.Reveal() != "google-s3cret" {
t.Errorf("GoogleClientSecret.Reveal() = %q", cfg.GoogleClientSecret.Reveal())
}
if want := []string{"mealquest-admin", "family"}; !slices.Equal(cfg.OIDCAllowedGroups, want) {
t.Errorf("OIDCAllowedGroups = %q, want %q", cfg.OIDCAllowedGroups, want)
}
@@ -492,7 +538,7 @@ func TestSecretRedacted(t *testing.T) {
s := Secret{value: "s3cret-value"}
for _, format := range []string{"%v", "%s", "%+v", "%#v"} {
cfg := Config{SecretKey: s, OIDCClientSecret: s}
cfg := Config{SecretKey: s, OIDCClientSecret: s, GoogleClientSecret: s}
for _, out := range []string{fmt.Sprintf(format, s), fmt.Sprintf(format, cfg)} {
if strings.Contains(out, "s3cret-value") {
t.Errorf("format %s leaked secret: %s", format, out)