Compare commits

..

1 Commits

Author SHA1 Message Date
0daf413b47 Prevent sole admin from deleting their account
Without this guard, the last admin in the system could delete their own
account, making the application unmanageable. This adds a model method
`User#sole_admin?`, a controller guard in `RegistrationsController#destroy`,
and disables the delete button in the profile edit view when the current
user is the only remaining admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 06:52:14 +00:00
12 changed files with 210 additions and 360 deletions

View File

@@ -1,74 +0,0 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test-sqlite:
name: Tests (SQLite)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
env:
BUNDLE_WITH: "sqlite:development:test"
- name: Set up test database
run: bin/rails db:create db:schema:load
env:
RAILS_ENV: test
- name: Run tests
run: bin/rails test
env:
RAILS_ENV: test
CI: "true"
test-mysql:
name: Tests (MySQL)
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: fixin_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
env:
BUNDLE_WITH: "mysql:development:test"
- name: Set up test database
run: bin/rails db:schema:load
env:
RAILS_ENV: test
DB_ADAPTER: mysql
- name: Run tests
run: bin/rails test
env:
RAILS_ENV: test
CI: "true"
DB_ADAPTER: mysql

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path fill="#ffffff" d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" /></svg>

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 167 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path fill="#ffffff" d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" /></svg>

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -18,14 +18,10 @@
/* Strive for simplicity:
* * style elements/tags only - if possible,
* * replace element/tag name with class name - if element has to be styled
* differently depending on context (e.g. <form>; <a> as link/button),
* * styles with multiple selectors should have all selectors with same
* specificity, to allow proper rule specificity vs order management.
* differently depending on context (e.g. form)
*
* NOTE: style in a modular way, similar to how CSS @scope would be used,
* to make transition easier once @scope is widely available. */
/* TODO: review styles with multiple selectors and try to convert them to the same
* specificity. */
* NOTE: Style in a modular way, similar to how CSS @scope would be used,
* to make transition easier once @scope is widely available */
:root {
--color-focus-gray: #f3f3f3;
--color-border-gray: #dddddd;
@@ -57,26 +53,12 @@
:focus-visible {
outline: none;
}
/* NOTE: move to higher priority layer instead of using !important? */
[disabled] {
border-color: var(--color-border-gray) !important;
color: var(--color-border-gray) !important;
/* NOTE: cannot set cursor with `pointer-events: none`; can be fixed by setting
* `cursor` on wrapping element.
cursor: not-allowed; */
fill: var(--color-border-gray) !important;
pointer-events: none !important;
}
/* [hidden] submit elements cannot have `display` set as it makes them visible. */
[hidden] {
display: none !important;
}
/* Color coding of input controls' background:
* blue - target for interaction with pointer,
* gray - target for interaction with keyboard,
* red - destructive, non-undoable action.
* blue - target for interaction with pointer
* gray - target for interaction with keyboard
* red - destructive, non-undoable action
*/
button,
details,
@@ -91,17 +73,50 @@ input,
select {
text-align: inherit;
}
a,
button,
input[type=submit] {
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
/* [hidden] submit controls cannot have `display` set as it makes them visible */
.button,
button:not([hidden]),
input[type=submit]:not([hidden]),
.tab {
align-items: center;
color: var(--color-gray);
display: flex;
fill: var(--color-gray);
font-weight: bold;
}
.button,
button,
input[type=submit] {
font-size: 0.8rem;
padding: 0.6em 0.5em;
width: fit-content;
}
input:not([type=submit]):not([type=checkbox]),
select,
summary,
textarea {
padding: 0.2em 0.4em;
}
.button,
button,
input,
select,
summary,
textarea {
border: 1px solid var(--color-gray);
border: solid 1px var(--color-gray);
border-radius: 0.25em;
padding: 0.2em 0.4em;
}
input[type=checkbox],
svg,
textarea {
margin: 0;
margin: 0
}
input[type=checkbox] {
accent-color: var(--color-blue);
@@ -109,16 +124,13 @@ input[type=checkbox] {
-webkit-appearance: none;
display: flex;
height: 1.1em;
margin: 0;
padding: 0;
width: 1.1em;
}
input[type=checkbox]:checked {
appearance: checkbox;
-webkit-appearance: checkbox;
}
/* Hide spin buttons of <input type=number>. */
/* TODO: add spin buttons inside <input type=number>: before (-) and after (+) input. */
/* Hide spin buttons in input number fields */
input[type=number] {
appearance: textfield;
-moz-appearance: textfield;
@@ -130,80 +142,37 @@ input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.button > svg,
.tab > svg,
button > svg {
height: 1.4em;
width: 1.4em;
}
.button > svg:not(:last-child),
.tab > svg:not(:last-child),
button > svg:not(:last-child) {
margin-right: 0.2em;
}
/* TODO: move normal non-button links (<a>:hover/:focus) styling here (i.e.
* page-wide, top-level) and remove from table.items - as the style should be
* same everywhere */
.button:focus-visible,
button:focus-visible,
input[type=submit]:focus-visible {
background-color: var(--color-focus-gray);
}
input:focus-visible,
select:focus-visible,
select:focus-within,
/* TODO: how to achieve `summary:focus-within` for `::details-content`? */
/* TODO: how to achieve summary:focus-within for ::details-content? */
summary:focus-visible,
textarea:focus-visible {
accent-color: var(--color-dark-blue);
background-color: var(--color-focus-gray);
}
input:hover,
select:hover,
summary:hover,
textarea:hover {
border-color: var(--color-blue);
outline: 1px solid var(--color-blue);
}
select:hover,
summary:hover {
cursor: pointer;
}
input:invalid,
select:invalid,
textarea:invalid {
border-color: var(--color-red);
outline-color: var(--color-red);
}
/* `.button`: button-styled <a>, <button>, <input type=submit>.
* `.link`: any other <a>.
* `.tab`: tab-styled <a>.
*/
.button,
.link,
.tab {
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
.button,
.tab {
align-items: center;
color: var(--color-gray);
display: flex;
fill: var(--color-gray);
font-weight: bold;
}
.button {
border: 1px solid var(--color-gray);
border-radius: 0.25em;
font-size: 0.8rem;
padding: 0.6em 0.5em;
width: fit-content;
}
[name=cancel],
.auxiliary {
border-color: var(--color-border-gray);
color: var(--color-nav-gray);
fill: var(--color-nav-gray);
}
.button > svg,
.tab > svg {
height: 1.4em;
width: 1.4em;
}
.button > svg:not(:last-child),
.tab > svg:not(:last-child) {
margin-right: 0.2em;
}
.button:focus-visible,
.tab:focus-visible,
.tab:hover {
background-color: var(--color-focus-gray);
}
.button:hover {
.button:hover,
button:hover,
input[type=submit]:hover {
background-color: var(--color-blue);
border-color: var(--color-blue);
color: white;
@@ -213,9 +182,23 @@ textarea:invalid {
background-color: var(--color-red);
border-color: var(--color-red);
}
/* TODO: move normal, non-button links (<a>:hover/:focus) styling here (i.e.
* page-wide, top-level) and remove from `table.items` - as the style should be
* same everywhere. */
input:hover,
select:hover,
summary:hover,
textarea:hover {
border-color: var(--color-blue);
outline: solid 1px var(--color-blue);
}
select:hover,
summary:hover {
cursor: pointer;
}
input:invalid,
select:invalid,
textarea:invalid {
border-color: var(--color-red);
outline: solid 1px var(--color-red);
}
input[type=text]:read-only,
textarea:read-only {
border: none;
@@ -223,8 +206,8 @@ textarea:read-only {
}
/* NOTE: collapse gaps around empty rows (`topside`) once possible with
* `grid-collapse` property and remove alternative `grid-template-areas`.
/* NOTE: collapse gaps around empty rows (`topside`) once possible
* with grid-collapse property and remove alternative grid-template
* https://github.com/w3c/csswg-drafts/issues/5813 */
body {
display: grid;
@@ -257,14 +240,18 @@ header {
margin-inline-start: 4%;
}
.navigation > .tab {
border-bottom: 2px solid var(--color-nav-gray);
border-bottom: solid 2px var(--color-nav-gray);
flex: 1;
font-size: 1rem;
justify-content: center;
padding-block: 0.4em;
}
.navigation > .tab:hover,
.navigation > .tab:focus-visible {
background-color: var(--color-focus-gray);
}
.navigation > .tab.active {
border-bottom: 4px solid var(--color-blue);
border-bottom: solid 4px var(--color-blue);
color: var(--color-blue);
fill: var(--color-blue);
}
@@ -296,7 +283,7 @@ header {
#flashes {
display: grid;
row-gap: 0.4em;
gap: 0.2em;
grid-template-columns: 1fr auto auto auto 1fr;
left: 0;
pointer-events: none;
@@ -312,17 +299,13 @@ header {
display: grid;
grid-column: 2/5;
grid-template-columns: subgrid;
line-height: 2.2em;
pointer-events: auto;
}
.flash:before {
filter: invert();
height: 1.4em;
margin: 0 0.5em;
width: 1.4em;
}
.flash.alert:before {
content: url('pictograms/alert-outline.svg');
height: 1.4em;
margin: 0 0.5em;
width: 1.4em;
}
.flash.alert {
border-color: var(--color-red);
@@ -330,24 +313,34 @@ header {
}
.flash.notice:before {
content: url('pictograms/check-circle-outline.svg');
height: 1.4em;
margin: 0 0.5em;
width: 1.4em;
}
.flash.notice {
border-color: var(--color-blue);
background-color: var(--color-blue);
}
.flash svg {
cursor: pointer;
fill: white;
height: 2.2em;
opacity: 0.6;
padding: 0.4em 0.5em;
width: 2.4em;
.flash > div {
grid-column: 2;
}
.flash svg:hover {
/* NOTE: currently flash button inherits some unnecessary styles from generic
* button. */
.flash > button {
border: none;
color: inherit;
cursor: pointer;
font-size: 1.4em;
font-weight: bold;
grid-column: 3;
opacity: 0.6;
padding: 0.2em 0.4em;
}
.flash > button:hover {
opacity: 1;
}
/* TODO: Hover over invalid should work like in measurements (thin vs thick border) */
.labeled-form {
align-items: center;
display: grid;
@@ -364,7 +357,7 @@ header {
.labeled-form label.required {
font-weight: bold;
}
/* Don't style `label.error + input` if case already covered by `input:invalid`. */
/* Don't style `label.error + input` if case already covered by input:invalid */
.labeled-form label.error {
color: var(--color-red);
}
@@ -378,23 +371,15 @@ header {
}
.labeled-form input[type=submit] {
font-size: 1rem;
margin: 1em auto 0 auto;
margin: 1.5em auto 0 auto;
padding: 0.75em;
}
.labeled-form .auxiliary {
grid-column: 3;
/* If more buttons are needed, `grid-row` can be replaced with
* `reading-flow: grid-columns` to ensure proper [tabindex] order. */
grid-row: 1;
height: 100%;
padding-block: 0;
}
/* TODO: remove `.items` class (?) and make `form table` work properly. */
/* TODO: remove .items class (?) and make 'form table' work properly */
table.items {
border-spacing: 0;
border: 1px solid var(--color-border-gray);
border: solid 1px var(--color-border-gray);
border-radius: 0.25em;
font-size: 0.85rem;
text-align: left;
@@ -417,7 +402,7 @@ table.items th,
table.items td {
padding-inline: 1em 0;
}
/* For <a> to fill <td> completely, we use an `::after` pseudoelement. */
/* For <a> to fill <td> completely, we use an ::after pseudoelement. */
table.items td.link {
padding: 0;
position: relative;
@@ -447,7 +432,7 @@ table.items td:last-child {
padding-inline-end: 0.1em;
}
table.items td {
border-top: 1px solid var(--color-border-gray);
border-top: solid 1px var(--color-border-gray);
height: 2.4em;
padding-block: 0.1em;
}
@@ -466,7 +451,7 @@ table.items tr.dropzone::after {
content: '';
inset: 1px 0 0 0;
position: absolute;
outline: 2px dashed var(--color-blue);
outline: dashed 2px var(--color-blue);
outline-offset: -1px;
z-index: var(--z-index-table-row-outline);
}
@@ -477,8 +462,8 @@ table.items tr.form td {
vertical-align: top;
}
/* TODO: replace `:hover:focus-visible` combos with proper LOVE style order. */
/* TODO: update table styling: simplify selectors, deduplicate, remove non-font rem. */
/* TODO: replace :hover:focus-visible combos with proper LOVE stye order */
/* TODO: Update table styling: simplify selectors, deduplicate, remove non-font rem. */
table.items td.link a:hover,
table.items td.link a:focus-visible,
table.items td.link a:hover:focus-visible {
@@ -516,7 +501,9 @@ table.items td.svg {
table.items td.number {
text-align: right;
}
table.items .button {
table.items .button,
table.items button,
table.items input[type=submit] {
font-weight: normal;
height: 100%;
padding: 0.4em;
@@ -526,26 +513,30 @@ table.items select,
table.items textarea {
padding-block: 0.375em;
}
table input,
table select,
table summary,
table textarea {
/* TODO: find a way (layers?) to style inputs differently while making sure
* hover works properly without using :not(:hover) selectors here. */
table.items .button:not(:hover),
table.items button:not(:hover),
table.items input:not(:hover),
table.items select:not(:hover),
table.items textarea:not(:hover) {
border-color: var(--color-border-gray);
}
table select {
table.items .button:not(:hover),
table.items button:not(:hover),
table.items input[type=submit]:not(:hover),
table.items select:not(:hover) {
color: var(--color-table-gray);
}
table select:hover,
table select:focus-within,
table select:focus-visible {
table.items select:focus-within,
table.items select:focus-visible {
color: black;
}
table .button {
form a[name=cancel] {
border-color: var(--color-border-gray);
color: var(--color-table-gray);
color: var(--color-nav-gray);
fill: var(--color-nav-gray);
}
form table.items {
border: none;
}
@@ -557,9 +548,6 @@ form table.items td {
form table.items td:first-child {
color: inherit;
}
form table select {
color: black;
}
.centered {
@@ -592,6 +580,17 @@ form table select {
gap: 0.8em;
flex-direction: column;
}
[disabled] {
/* label:has(input[disabled]) {
* TODO: disabled checkbox blue square focus removal; disabled label styling;
* focused label styling (currently only checkbox has focus)
* */
border-color: var(--color-border-gray) !important;
color: var(--color-border-gray) !important;
cursor: not-allowed;
fill: var(--color-border-gray) !important;
pointer-events: none;
}
details {
@@ -619,7 +618,7 @@ summary:has(.button) {
padding-inline-end: 0;
}
summary .button {
border: 1px solid var(--color-border-gray);
border: solid 1px var(--color-border-gray);
border-radius: inherit;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
@@ -635,10 +634,10 @@ details[open] summary::before {
summary::marker {
padding-left: 0.25em;
}
/* NOTE: use `details[open]::details-content` once widely available. */
/* NOTE: use details[open]::details-content once widely available */
details[open] ul {
background: white;
border: 1px solid var(--color-border-gray);
border: solid 1px var(--color-border-gray);
border-radius: 0.25em;
box-shadow: 1px 1px 3px var(--color-border-gray);
margin: -1px 0 0 0;
@@ -660,10 +659,3 @@ li input[type=checkbox] {
li::marker {
content: '';
}
/*
* TODO:
* * disable <label> containing disabled checkbox: `label:has(input[disabled])`,
* * disabled label styling,
* * focused label styling (currently only checkbox has focus),
* * disabled checkbox blue square focus removal.
* */

View File

@@ -12,12 +12,6 @@ module ApplicationHelper
labeled_field_for(method, options) { super }
end
def submit(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash)
options[:class] = @template.class_names('button', options[:class])
super
end
private
def labeled_field_for(method, options)
@@ -78,8 +72,13 @@ module ApplicationHelper
end
def labeled_form_for(record, options = {}, &block)
extra_options = {builder: LabeledFormBuilder, html: {class: 'labeled-form'}}
form_for(record, **merge_attributes(options, extra_options), &block)
extra_options = {builder: LabeledFormBuilder,
data: {turbo: false},
html: {class: 'labeled-form'}}
options = options.deep_merge(extra_options) do |key, left, right|
key == :class ? class_names(left, right) : right
end
form_for(record, **options, &block)
end
class TabularFormBuilder < ActionView::Helpers::FormBuilder
@@ -114,12 +113,8 @@ module ApplicationHelper
end
def button(value = nil, options = {}, &block)
# #button does not use #objectify_options/@default_options
value, options = nil, value if value.is_a?(Hash)
options = options.merge(
@default_options.slice(:form),
class: @template.class_names('button', options[:class])
)
# button does not use #objectify_options
options.merge!(@options.slice(:form))
super
end
@@ -140,21 +135,20 @@ module ApplicationHelper
# [autofocus]. Otherwise IDs are not unique when multiple forms are open
# and the first input gets focus.
record_object, options = nil, record_object if record_object.is_a?(Hash)
extra_options = {builder: TabularFormBuilder, skip_default_ids: true}
options = merge_attributes(options, extra_options)
options.merge!(builder: TabularFormBuilder, skip_default_ids: true)
# TODO: set error message with setCustomValidity instead of rendering to flash?
render_errors(record_object || record_name)
fields_for(record_name, record_object, **options, &block)
end
def tabular_form_with(**options, &block)
extra_options = {builder: TabularFormBuilder, html: {autocomplete: 'off'}}
form_with(**merge_attributes(options, extra_options), &block)
options = options.deep_merge(builder: TabularFormBuilder,
html: {autocomplete: 'off'})
form_with(**options, &block)
end
def svg_tag(source, label = nil, options = {})
label, options = nil, label if label.is_a? Hash
svg_tag = tag.svg(**options) do
svg_tag = tag.svg(options) do
tag.use(href: "#{image_path(source + ".svg")}#icon")
end
label.blank? ? svg_tag : svg_tag + tag.span(label)
@@ -165,7 +159,6 @@ module ApplicationHelper
['measurements', 'scale-bathroom', :restricted],
['quantities', 'axis-arrow', :restricted, 'right'],
['units', 'weight-gram', :restricted],
# TODO: display users tab only if >1 user present; sole_user?/sole_admin?
['users', 'account-multiple-outline', :admin],
]
@@ -213,7 +206,6 @@ module ApplicationHelper
def render_errors(records)
# Conversion of flash to Array only required because of Devise
# TODO: override Devise message setting to Array()?
flash[:alert] = Array(flash[:alert])
Array(records).each { |record| flash[:alert] += record.errors.full_messages }
end
@@ -223,8 +215,8 @@ module ApplicationHelper
# Conversion of flash to Array only required because of Devise
Array(messages).map do |message|
tag.div class: "flash #{entry}" do
tag.span(sanitize(message)) +
svg_tag('pictograms/close-outline', {onclick: "this.parentElement.remove()"})
tag.div(sanitize(message)) + tag.button(sanitize("&times;"), tabindex: -1,
onclick: "this.parentElement.remove();")
end
end
end.join.html_safe
@@ -260,11 +252,4 @@ module ApplicationHelper
[name, html_options]
end
# Like Hash#deep_merge, but aware of HTML attributes.
def merge_attributes(left, right)
left.deep_merge(right) do |key, lvalue, rvalue|
key == :class ? class_names(lvalue, rvalue) : rvalue
end
end
end

View File

@@ -26,8 +26,10 @@ class Unit < ApplicationRecord
other_bases_units = arel_table.alias('other_bases_units')
sub_units = arel_table.alias('sub_units')
# TODO: move inner 'with' CTE to outer 'with recursive' - it can have multiple
# CTEs, even non recursive ones.
Unit.with_recursive(actionable_units: [
self.or(Unit.defaults).left_joins(:base)
Unit.with(units: self.or(Unit.defaults)).left_joins(:base)
.where.not(
# Exclude Units that are/have default counterpart
Arel::SelectManager.new.project(1).from(other_units)
@@ -63,14 +65,8 @@ class Unit < ApplicationRecord
),
# Fill base Units to display proper hierarchy. Duplicates will be removed
# by final group() - can't be deduplicated with UNION due to 'portable' field.
# Use ActiveRecord::Relation (not a raw SelectManager) so the SQLite Arel
# visitor does not wrap it in parentheses inside the UNION ALL CTE body.
Unit.joins(
arel_table.create_join(
actionable_units,
arel_table.create_on(actionable_units[:base_id].eq(arel_table[:id]))
)
).select(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable'))
arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id]))
.project(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable'))
]).select(units: [:base_id, :symbol])
.select(
units[:id].minimum.as('id'), # can be ANY_VALUE()
@@ -78,7 +74,7 @@ class Unit < ApplicationRecord
Arel::Nodes.build_quoted(1).as('multiplier'), # disregard multiplier when sorting
units[:portable].minimum.as('portable')
)
.from(units).group(units[:base_id], units[:symbol])
.from(units).group(:base_id, :symbol)
}
scope :ordered, ->{
left_outer_joins(:base).order(ordering)
@@ -87,8 +83,8 @@ class Unit < ApplicationRecord
def self.ordering
[arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]),
arel_table[:base_id].not_eq(nil),
arel_table[:multiplier],
arel_table[:symbol]]
:multiplier,
:symbol]
end
before_destroy do

View File

@@ -1 +1 @@
OOGMGhfQuV67kqlMecZLcNfgrGS81KPAGmY27GnohtcGSPtiaqL8OZYsVf5IIOaI1K14ZEflln+E2deaIJ5apaq98f+1gJawGbAJeEfLCskJV/03nT8ICpRk+bxT/lzqeCIaUJOLk4708ufC9EpdpJD/jgVSAuI/iNMzzwMbNFvqNmx0Kmgp0mRpHSDGLZZkaP3GW7wdsJEVsNpSPIrkkGL1BvD+nHbmHjuGkn4MMsmm1Yz0M31jkJiDksT0SVeOcxWvOApclxm6VZOAws2l6YKEs/XoE7ye3ssjxdjdwjMzRXV7dwYclNBQGRoERVTozdYiFR4eAGMdlG0RsnUAp+edILH5nvHCIPb3la/dUTOzAQMNY0TqMMVUHGqGuVS/EMCX/w7zrmYN5C+2W8SfugrvTpAL--dUdvsDfbUdVrbmmo--fUwsWUp+DQGPtEF+Zq4ZTw==
3nm9KZNtyLhPgZBVzOOkN2FXHD0uEMuzgb5Sl1MrAMmi6+iEFSzyTHfZFW2mz18VyNz5DDYvTODZqBDQKK+FQh70uEQkmGqaY5XsTOzUFzk56quaPNtZvFEGux1nX2avSbYQBs3HeyYyWyTAFhez5j8tVb6sZD2xZ8twa9KAB42j86NIHT9w/ZMFqZbGbdBoR1Mrqoy9/IWv2QgxMTpGR6JBpTUwauXm6wS/bTt8SCXF57JSVgvdw/BxFzoA3Xj6N5E89LbMfh54W2ruMhybka5E7zXN9z0v4oXt8GiYZFIODEYZwqzEVaUK1WXS5qb5OrDJFAzs29Uf/gDrIDx71Lot+jejCS+xFfI9454EnHcVH66wKuwF6ylKupJDffM0hQHplcEfVSq5UiDfbPXm46Vr0g1A--2RrmuzCBuHvYpPNA--ugbuRe7ivfDqeUCt6ahciA==

View File

@@ -58,7 +58,4 @@ Rails.application.configure do
# config.action_view.annotate_rendered_view_with_filenames = true
config.log_level = :info
# Allow the default integration test host.
config.hosts << "www.example.com"
end

View File

@@ -1,50 +0,0 @@
namespace :test do
desc "Run Rails tests against all supported database adapters (SQLite, MySQL)"
task :all_adapters do
# DATABASE_URL overrides the adapter from database.yml at runtime.
# MySQL requires the mysql2 gem: bundle install --with mysql
adapters = {
"SQLite" => {
"DATABASE_URL" => "sqlite3:db/test.sqlite3"
},
"MySQL" => {
"DATABASE_URL" => format(
"mysql2://%s:%s@%s/%s",
ENV.fetch("DATABASE_USERNAME", "root"),
ENV.fetch("DATABASE_PASSWORD", ""),
ENV.fetch("DATABASE_HOST", "127.0.0.1"),
ENV.fetch("DATABASE_NAME", "fixin_test")
)
}
}
failed = []
adapters.each do |name, extra_env|
puts "\n#{"=" * 60}"
puts " Running tests with #{name}"
puts "=" * 60
env = ENV.to_h.merge("RAILS_ENV" => "test").merge(extra_env)
# Reset test database; db:drop may fail on first run — that's fine
system(env, "bin/rails db:drop")
unless system(env, "bin/rails db:create db:schema:load")
failed << "#{name} (database setup)"
next
end
failed << name unless system(env, "bin/rails test")
end
puts "\n#{"=" * 60}"
if failed.any?
puts " FAILED: #{failed.join(", ")}"
puts "=" * 60
exit 1
else
puts " All adapters passed!"
puts "=" * 60
end
end
end

View File

@@ -1,12 +1,8 @@
require "test_helper"
class Default::UnitsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:alice)
end
test "should get index" do
get default_units_url
get units_defaults_index_url
assert_response :success
end
end

View File

@@ -2,9 +2,7 @@ require "test_helper"
class UsersControllerTest < ActionDispatch::IntegrationTest
setup do
@admin = users(:admin)
@user = users(:alice)
sign_in @admin
@user = users(:one)
end
test "should get index" do
@@ -12,25 +10,39 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "should get new" do
get new_user_url
assert_response :success
end
test "should create user" do
assert_difference("User.count") do
post users_url, params: { user: { email: @user.email, status: @user.status } }
end
assert_redirected_to user_url(User.last)
end
test "should show user" do
get user_url(@user)
assert_response :success
end
test "should get edit" do
get edit_user_url(@user)
assert_response :success
end
test "should update user" do
patch user_url(@user), params: { user: { status: :restricted } }, as: :turbo_stream
assert_equal "restricted", @user.reload.status
patch user_url(@user), params: { user: { email: @user.email, status: @user.status } }
assert_redirected_to user_url(@user)
end
test "should not update self" do
patch user_url(@admin), params: { user: { status: :active } }, as: :turbo_stream,
headers: { "HTTP_REFERER" => users_url }
assert_response :redirect
end
test "should destroy user" do
assert_difference("User.count", -1) do
delete user_url(@user)
end
test "should forbid non-admin" do
sign_in @user
get users_url
assert_response :forbidden
assert_redirected_to users_url
end
end

View File

@@ -2,10 +2,6 @@ ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
class ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
end
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)