@@ -111,6 +111,8 @@ interface Backend {
111111 changes : { at : string ; actor : string ; action : string ; role_id : string ; role_name : string ; detail : string } [ ] ;
112112 /** You hold this custom role (your record's role); requests run as `selfRole`. */
113113 selfCustom ?: string ;
114+ /** GitHub sign-in works here (`GET /auth/methods`). */
115+ githubSignIn ?: boolean ;
114116}
115117
116118// The gateway's rules for custom roles, as the mock answers them.
@@ -140,6 +142,22 @@ async function mockApi(page: Page, selfRole = "admin", init: Partial<Backend> =
140142 const body = req . postData ( ) ? ( JSON . parse ( req . postData ( ) as string ) as Record < string , unknown > ) : null ;
141143 db . calls . push ( { method, path : p , body, search : url . search } ) ;
142144 const self = db . members . find ( ( m ) => m . is_self ) ;
145+ if ( p === "/auth/methods" ) return json ( route , { password : true , google : false , github : db . githubSignIn ?? false } ) ;
146+ if ( p === "/api/users/setup-emails" && method === "POST" ) {
147+ if ( LEVEL [ selfRole ] < 3 ) return json ( route , { error : "Only admins and owners can do that." } , 403 ) ;
148+ const hour = Date . now ( ) - 3600_000 ;
149+ const sent_at : Record < string , string > = { } ;
150+ let skipped = 0 ;
151+ for ( const m of db . members . filter ( ( x ) => x . needs_setup ) ) {
152+ if ( m . setup_email_sent_at && new Date ( m . setup_email_sent_at ) . getTime ( ) > hour ) {
153+ skipped += 1 ;
154+ continue ;
155+ }
156+ m . setup_email_sent_at = new Date ( ) . toISOString ( ) ;
157+ sent_at [ m . user_id ] = m . setup_email_sent_at ;
158+ }
159+ return json ( route , { sent : Object . keys ( sent_at ) . length , skipped_recent : skipped , failed : 0 , sent_at } ) ;
160+ }
143161 if ( p === "/api/me" ) return json ( route , { user_id : self ?. user_id ?? "u-admin" , team_id : "t_1" , github_login : null , email : self ?. email ?? "" , avatar_url : "" , role : selfRole } ) ;
144162 if ( p === "/api/usage" ) return json ( route , { month : "2026-01" , total_tokens : 0 , tokens_in : 0 , tokens_out : 0 , total_runs : 0 , max_tokens : 0 } ) ;
145163 if ( p === "/api/banners" ) return json ( route , { banners : [ ] } ) ;
@@ -483,20 +501,79 @@ test("self-hosted: a member who never signed in here gets a set-up email from th
483501 const mia = db . members . find ( ( x ) => x . user_id === "u-mem" ) ! ;
484502 mia . needs_setup = true ;
485503 await page . goto ( "/settings/team" ) ;
486- await expect ( row ( page , "Mia Member" ) . getByTestId ( "setup-status" ) ) . toHaveText ( "Hasn't set up an account here yet" ) ;
504+ await expect ( row ( page , "Mia Member" ) . getByTestId ( "setup-status" ) ) . toHaveText ( "Hasn't signed in here yet" ) ;
487505 // Only that member: everyone else has signed in here.
488506 await expect ( members ( page ) . getByTestId ( "setup-status" ) ) . toHaveCount ( 1 ) ;
507+ await expect ( row ( page , "Mia Member" ) . getByRole ( "button" , { name : "Send set-up email" } ) ) . toBeVisible ( ) ;
489508 await row ( page , "Mia Member" ) . getByRole ( "button" , { name : "More actions for Mia Member" } ) . click ( ) ;
490509 await page . getByRole ( "menuitem" , { name : "Send set-up email" } ) . click ( ) ;
491510 await expect ( page . getByText ( "Set-up email sent to mia@example.com" ) ) . toBeVisible ( ) ;
492- await expect ( row ( page , "Mia Member" ) . getByTestId ( "setup-status" ) ) . toHaveText ( / ^ S e t - u p e m a i l s e n t / ) ;
511+ await expect ( row ( page , "Mia Member" ) . getByTestId ( "setup-sent" ) ) . toHaveText ( / ^ S e t - u p e m a i l s e n t / ) ;
512+ await expect ( row ( page , "Mia Member" ) . getByRole ( "button" , { name : "Send the set-up email again" } ) ) . toBeVisible ( ) ;
493513 expect ( db . calls . some ( ( c ) => c . method === "POST" && c . path === "/api/users/u-mem/setup-email" ) ) . toBe ( true ) ;
494514 // Sent before: the menu says it goes again.
495515 await row ( page , "Mia Member" ) . getByRole ( "button" , { name : "More actions for Mia Member" } ) . click ( ) ;
496516 await expect ( page . getByRole ( "menuitem" , { name : "Send the set-up email again" } ) ) . toBeVisible ( ) ;
497517 await shot ( page , "team-setup-email" ) ;
498518} ) ;
499519
520+ test ( "self-hosted: someone copied over shows they haven't signed in here, not their old activity" , async ( { page } ) => {
521+ const db = await mockApi ( page , "admin" , { githubSignIn : true } ) ;
522+ db . members . push (
523+ member ( { user_id : "u-al" , email : "al.chan@acme.example" , name : "Al Chan" , github_login : "achan" , github_id : "777" , sign_in : "email_and_github" , last_active_at : ago ( 5 * DAY ) , needs_setup : true } ) ,
524+ ) ;
525+ await page . goto ( "/settings/team" ) ;
526+ const al = row ( page , "Al Chan" ) ;
527+ await expect ( al . getByTestId ( "setup-status" ) ) . toHaveText ( "Hasn't signed in here yet" ) ;
528+ await expect ( al ) . not . toContainText ( "Active 5 days ago" ) ;
529+ await expect ( al . getByTestId ( "previous-activity" ) ) . toHaveText ( "Last active on the previous Coderhelm, 5 days ago" ) ;
530+ await expect ( al . getByTestId ( "github-sign-in" ) ) . toHaveText ( "Can sign in with GitHub (achan)" ) ;
531+ await expect ( al . getByRole ( "button" , { name : "Send set-up email" } ) ) . toBeVisible ( ) ;
532+ // People who signed in here keep their plain activity time.
533+ await expect ( row ( page , "Sam Rivera" ) ) . toContainText ( "Active 5 days ago" ) ;
534+ await expect ( page . getByText ( "· 1 haven't signed in here yet" ) ) . toBeVisible ( ) ;
535+ } ) ;
536+
537+ test ( "self-hosted: without GitHub sign-in, no GitHub line for someone copied over" , async ( { page } ) => {
538+ const db = await mockApi ( page , "admin" , { githubSignIn : false } ) ;
539+ db . members . push ( member ( { user_id : "u-al" , email : "al.chan@acme.example" , name : "Al Chan" , github_login : "achan" , github_id : "777" , needs_setup : true } ) ) ;
540+ await page . goto ( "/settings/team" ) ;
541+ await expect ( row ( page , "Al Chan" ) . getByTestId ( "setup-status" ) ) . toBeVisible ( ) ;
542+ await expect ( row ( page , "Al Chan" ) . getByTestId ( "github-sign-in" ) ) . toHaveCount ( 0 ) ;
543+ } ) ;
544+
545+ test ( "self-hosted: the banner counts who hasn't set up an account and sends to all, skipping the last hour" , async ( { page } ) => {
546+ const db = await mockApi ( page ) ;
547+ const mia = db . members . find ( ( x ) => x . user_id === "u-mem" ) ! ;
548+ const jo = db . members . find ( ( x ) => x . user_id === "u-jo" ) ! ;
549+ const sam = db . members . find ( ( x ) => x . user_id === "u-sam" ) ! ;
550+ mia . needs_setup = true ;
551+ sam . needs_setup = true ;
552+ jo . needs_setup = true ;
553+ jo . setup_email_sent_at = ago ( 20 * 60_000 ) ;
554+ await page . goto ( "/settings/team" ) ;
555+ const banner = page . getByTestId ( "setup-banner" ) ;
556+ await expect ( banner ) . toContainText ( "3 people haven't set up an account here yet" ) ;
557+ await expect ( page . getByText ( "· 3 haven't signed in here yet" ) ) . toBeVisible ( ) ;
558+ await banner . getByRole ( "button" , { name : "Send set-up emails to all" } ) . click ( ) ;
559+ await expect ( page . getByText ( "Set-up email sent to 2 people. 1 person got one in the last hour and was skipped." ) ) . toBeVisible ( ) ;
560+ expect ( db . calls . filter ( ( c ) => c . method === "POST" && c . path === "/api/users/setup-emails" ) ) . toHaveLength ( 1 ) ;
561+ await expect ( row ( page , "Mia Member" ) . getByTestId ( "setup-sent" ) ) . toHaveText ( / ^ S e t - u p e m a i l s e n t / ) ;
562+ await expect ( row ( page , "Sam Rivera" ) . getByTestId ( "setup-sent" ) ) . toHaveText ( / ^ S e t - u p e m a i l s e n t / ) ;
563+ await shot ( page , "team-setup-banner" ) ;
564+ } ) ;
565+
566+ test ( "the set-up banner has no send button for someone who can't invite" , async ( { page } ) => {
567+ const db = await mockApi ( page , "member" ) ;
568+ db . members . find ( ( x ) => x . user_id === "u-sam" ) ! . needs_setup = true ;
569+ await page . goto ( "/settings/team" ) ;
570+ const banner = page . getByTestId ( "setup-banner" ) ;
571+ await expect ( banner ) . toContainText ( "1 person hasn't set up an account here yet" ) ;
572+ await expect ( banner ) . toContainText ( "Ask an admin to send them the set-up email." ) ;
573+ await expect ( banner . getByRole ( "button" , { name : "Send set-up emails to all" } ) ) . toHaveCount ( 0 ) ;
574+ await expect ( row ( page , "Sam Rivera" ) . getByRole ( "button" , { name : "Send set-up email" } ) ) . toHaveCount ( 0 ) ;
575+ } ) ;
576+
500577test ( "no set-up email for members who signed in, or for someone who can't invite" , async ( { page } ) => {
501578 await mockApi ( page ) ;
502579 await page . goto ( "/settings/team" ) ;
0 commit comments