package cabana_test import ( "bytes" "context" "net/http" "testing" "time" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/cabana" ) func TestBackendTokenPostgres(t *testing.T) { gdb := adminGorm(t) if err := gdb.AutoMigrate(&tokenDemoWidget{}); err != nil { t.Fatal(err) } h := tokenAdminHandler(t) user := insertAdmin(t, gdb, "tok-life", "tok-life@example.test", "secret-pass", true, false) listPath := adminAPI("/acme/demo/widgets") plain, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, nil) if err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK { t.Fatalf("initial: %d %s", rec.Code, rec.Body.String()) } // D-10: deactivate → 401, reactivate → 200. if err := gdb.Exec(`UPDATE backend_users SET is_activated = false WHERE id = ?`, user.ID).Error; err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusUnauthorized { t.Fatalf("deactivated: want 401, got %d %s", rec.Code, rec.Body.String()) } if err := gdb.Exec(`UPDATE backend_users SET is_activated = true WHERE id = ?`, user.ID).Error; err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK { t.Fatalf("reactivated: want 200, got %d %s", rec.Code, rec.Body.String()) } // Password reset / tokens_valid_after leave the personal API token working. reset := commandByName(t, cabana.RuntimeCommands(commandApp(t, gdb)), "admin:reset-password") var buf bytes.Buffer out := bonfire.NewOutput(nil, &buf, &buf) if err := reset.Run(context.Background(), flagInput{ args: []string{user.Login}, flags: map[string]string{"password": "new-secret-pass"}, }, out); err != nil { t.Fatalf("admin:reset-password: %v output=%s", err, buf.String()) } var afterReset cabana.BackendUser if err := gdb.First(&afterReset, user.ID).Error; err != nil { t.Fatal(err) } if afterReset.TokensValidAfter == nil { t.Fatal("reset did not set tokens_valid_after") } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK { t.Fatalf("after password reset: want 200, got %d %s", rec.Code, rec.Body.String()) } cutoff := time.Now().UTC().Add(time.Hour) if err := gdb.Model(&cabana.BackendUser{}).Where("id = ?", user.ID).Update("tokens_valid_after", cutoff).Error; err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusOK { t.Fatalf("after tokens_valid_after cutoff: want 200, got %d %s", rec.Code, rec.Body.String()) } // Revoked → 401. now := time.Now().UTC() if err := gdb.Exec(`UPDATE backend_api_tokens SET revoked_at = ? WHERE backend_user_id = ?`, now, user.ID).Error; err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain); rec.Code != http.StatusUnauthorized { t.Fatalf("revoked: want 401, got %d %s", rec.Code, rec.Body.String()) } // Fresh token, expired → 401. expired := now.Add(-time.Hour) plain2, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, &expired) if err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain2); rec.Code != http.StatusUnauthorized { t.Fatalf("expired: want 401, got %d %s", rec.Code, rec.Body.String()) } // Soft-delete owner → 401. plain3, err := cabana.IssueTestAPIToken(gdb, user.ID, []string{"demo"}, nil) if err != nil { t.Fatal(err) } if err := gdb.Delete(&user).Error; err != nil { t.Fatal(err) } if rec := getAuth(t, h, listPath, plain3); rec.Code != http.StatusUnauthorized { t.Fatalf("soft-deleted owner: want 401, got %d %s", rec.Code, rec.Body.String()) } // last_used_at stamped once within a minute for two requests. user2 := insertAdmin(t, gdb, "tok-stamp", "tok-stamp@example.test", "secret-pass", true, false) plain4, err := cabana.IssueTestAPIToken(gdb, user2.ID, []string{"demo"}, nil) if err != nil { t.Fatal(err) } var tokenID uint if err := gdb.Raw(`SELECT id FROM backend_api_tokens WHERE backend_user_id = ? ORDER BY id DESC LIMIT 1`, user2.ID).Scan(&tokenID).Error; err != nil || tokenID == 0 { t.Fatalf("token id: %d err=%v", tokenID, err) } if rec := getAuth(t, h, listPath, plain4); rec.Code != http.StatusOK { t.Fatalf("stamp first: %d %s", rec.Code, rec.Body.String()) } if rec := getAuth(t, h, listPath, plain4); rec.Code != http.StatusOK { t.Fatalf("stamp second: %d %s", rec.Code, rec.Body.String()) } var row cabana.BackendAPIToken if err := gdb.First(&row, tokenID).Error; err != nil { t.Fatal(err) } if row.LastUsedAt == nil { t.Fatal("last_used_at not set") } // Ensure the stamp path does not write plaintext scopes oddly. if !row.Scopes.Valid || len(row.Scopes.Get()) == 0 { t.Fatalf("scopes = %#v", row.Scopes) } }