Skip to content

868 add school email domain api#878

Open
PetarSimonovic wants to merge 8 commits into
mainfrom
868-add-school-email-domain-api
Open

868 add school email domain api#878
PetarSimonovic wants to merge 8 commits into
mainfrom
868-add-school-email-domain-api

Conversation

@PetarSimonovic

@PetarSimonovic PetarSimonovic commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Status

Description

A REST API that allows school owners and teachers to create and list SchoolEmailDomains.

  • Owners and teachers can list/create; students and users from other schools cannot
  • Domains are validated (FQDN format, URI, registrable public suffix) with translated error messages
  • New domains are synced to Profile; the DB transaction rolls back if Profile sync fails
  • Delete is out of scope for this PR

What's changed?

  • GET /api/schools/:school_id/school_email_domains — list domains
  • POST /api/schools/:school_id/school_email_domains — create a domain
  • Added concept with Profile sync + rollback on failure
  • Validation with translated error messages
  • Specs at validator, model, concept, and feature levels

@PetarSimonovic PetarSimonovic linked an issue Jun 17, 2026 that may be closed by this pull request
@cla-bot cla-bot Bot added the cla-signed label Jun 17, 2026
@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from b46e372 to 4ea2fbf Compare June 17, 2026 08:26
@PetarSimonovic PetarSimonovic requested a review from Copilot June 17, 2026 08:26
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Test coverage

91.93% line coverage reported by SimpleCov.
Run: https://github.com/RaspberryPiFoundation/editor-api/actions/runs/28161351896

@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from 4ea2fbf to 3159b2a Compare June 17, 2026 08:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a REST API for managing a school’s allowed student email domains, intended for use by authorised school owners/teachers and kept in sync with the Profile service.

Changes:

  • Introduces GET /api/schools/:school_id/school_email_domains (list) and POST /api/schools/:school_id/school_email_domains (create) endpoints.
  • Adds a SchoolEmailDomain::Create concept that persists the domain and syncs the school’s full domain list to Profile (rolling back on sync failure).
  • Extends authorisation and i18n error messages, plus adds factories and request/unit specs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
spec/support/profile_api_mock.rb Adds a stub helper for Profile “update school email domains” calls in specs.
spec/features/school_email_domain/listing_school_email_domains_spec.rb Request specs for listing domains with auth/unauth coverage.
spec/features/school_email_domain/creating_school_email_domains_spec.rb Request specs for creating domains, validation errors, and Profile failure handling.
spec/factories/school_email_domain.rb Factory for SchoolEmailDomain.
spec/concepts/school_email_domain/create_spec.rb Unit coverage for the SchoolEmailDomain::Create concept.
lib/concepts/school_email_domain/operations/create.rb Implements the create + Profile sync operation with rollback on failure.
config/routes.rb Wires nested school email domain routes under schools.
config/locales/en.yml Adds translated validation messages for domain validation error codes.
app/models/ability.rb Grants school owners/teachers ability to read/create school email domains.
app/controllers/api/school_email_domains_controller.rb Adds the API controller for index/create with CanCan authorisation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/concepts/school_email_domain/operations/create.rb
Comment thread lib/concepts/school_email_domain/operations/create.rb Outdated
Comment thread lib/concepts/school_email_domain/operations/create.rb
@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from 3159b2a to 1ef1f88 Compare June 17, 2026 09:54
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-u3xxpf June 17, 2026 09:54 Inactive
end
response
rescue ActiveRecord::RecordInvalid => e
record = response[:school_email_domain] || e.record

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth fixing this after all - I don't expect it to be a problem but it's a trivial change. It's similar to what's been done here.

@PetarSimonovic PetarSimonovic Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made this changed and initialised with nil since response[:school_email_domain] should be a single record

Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
Comment thread lib/concepts/school_email_domain/operations/create.rb Dismissed
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from 1ef1f88 to 511affb Compare June 17, 2026 10:54
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-u3xxpf June 17, 2026 10:54 Inactive
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-zp9tgf June 22, 2026 07:49 Inactive
Comment thread lib/concepts/school_email_domain/operations/create.rb Dismissed
Comment thread lib/concepts/school_email_domain/operations/create.rb Dismissed
Comment thread lib/concepts/school_email_domain/operations/create.rb Dismissed
Comment thread lib/concepts/school_email_domain/operations/create.rb Dismissed
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
Comment thread lib/concepts/school_email_domain/operations/create.rb Fixed
@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from 86c56f1 to e24d6af Compare June 22, 2026 07:54
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-n5eyni June 22, 2026 07:54 Inactive
@PetarSimonovic PetarSimonovic marked this pull request as ready for review June 22, 2026 14:00

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e24d6af. Configure here.

Comment thread lib/concepts/school_email_domain/operations/create.rb
@mwtrew mwtrew self-requested a review June 23, 2026 10:15
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-ppivrb June 23, 2026 13:18 Inactive
PetarSimonovic and others added 5 commits June 23, 2026 14:20
Add SchoolEmailDomainsController and school_email_domains route.
Return a list of domain strings.
Update CanCan ability to allow access only for teachers and owners.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add POST create to SchoolEmailDomainsController and route.
Add SchoolEmailDomain::Create concept to persist domains and sync the full list with Profile.
@PetarSimonovic PetarSimonovic force-pushed the 868-add-school-email-domain-api branch from a1bc812 to 27cc95f Compare June 23, 2026 13:20
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-ppivrb June 23, 2026 13:21 Inactive
Comment thread lib/tasks/test_seeds.rake
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-6dkc3a June 23, 2026 15:35 Inactive

@mwtrew mwtrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good overall, but I think we'll need to do something about the problem in this thread.

@mwtrew mwtrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks for those changes.

If you'd like to look into using an advisory lock that would be great, but I think what you have will work for now.

Serialise concurrent creates per school so Profile always receives a complete domain list.
@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-z7zr8l June 25, 2026 08:32 Inactive
Comment on lines +40 to +45
# Advisory lock: queue school email domain creation for the same school so Profile
# always gets a complete domain list. Released automatically on commit or rollback.
#
# lock_key is a CRC32 hash, so two different schools could theoretically share the same
# key (~1 in 4 billion per pair). That wouldn't corrupt data — it would only queue
# unrelated schools' updates briefly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to remove/shorten this comment if the point about identical key values is overkill

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think the comment can go

response
rescue StandardError => e
Sentry.capture_exception(e) # Send unexpected/Profile errors to Sentry
response[:error] = e.message
rescue StandardError => e
Sentry.capture_exception(e) # Send unexpected/Profile errors to Sentry
response[:error] = e.message
response[:error_code] = 'profile_sync_failed'
@PetarSimonovic PetarSimonovic requested a review from mwtrew June 25, 2026 08:36
described_class.call(school:, domain:, token:)

expect(school).to have_received(:with_lock)
lock_key = Zlib.crc32("#{SchoolEmailDomain::Create::LOCK_NAMESPACE}:#{school.id}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also check that the lock was released at the end?

@PetarSimonovic PetarSimonovic Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered a couple of approaches to check that the lock is released and I'm not sure it's possible to do this in a simple way for a couple of reasons.

1. Ask postgres

I thought the simplest would be to query the DB to see if any matching locks remain after the transaction.

But Rspec is configured to use_transactional_fixtures so I think the spec example itself is wrapped in a transaction. The advisory lock is only released once the spec has concluded so it's still present in the test.

2. pg_try_advisory_xact_lock

We could use pg_try_advisory_xact_lock to see if we can obtain a lock after our transaction has run. A false result would mean it was "still locked"

But within a given session I don't think we can ever get a false result: pg_try_advisory_xact_lock either acquires the lock, or returns true because the session already holds it.

Cursor suggestion

Cursor made the following suggestion about how we could practically check whether the lock has released. I'm happy to pursue it if it looks like a valid approach.

Use a hook in rails_helper.rb to disable transactional fixtures per-example:

Then create a new Thread to check out its own connection from the pool, which is a distinct PG session, so it can actually contend for the lock.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, didn't know that about Rspec tests. Yep, I was thinking of the first option as well.

I don't know if you've been able to test the contention situation manually - if you have and it works I think that's good enough, but if not it would be ideal to try it via a test like Cursor has suggested.

@PetarSimonovic PetarSimonovic temporarily deployed to editor-api-p-868-add-sc-5kpuzz June 25, 2026 09:43 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add school email domain API

3 participants